diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest
index 86eea1fbd4..b35d3593a3 100644
--- a/Ghidra/Features/Base/certification.manifest
+++ b/Ghidra/Features/Base/certification.manifest
@@ -351,6 +351,7 @@ src/main/help/help/topics/DataTypeManagerPlugin/data_type_manager_description.ht
src/main/help/help/topics/DataTypeManagerPlugin/data_type_manager_window.html||GHIDRA||||END|
src/main/help/help/topics/DataTypeManagerPlugin/images/CommitDialog.png||GHIDRA||||END|
src/main/help/help/topics/DataTypeManagerPlugin/images/DataTypeManager.png||GHIDRA||||END|
+src/main/help/help/topics/DataTypeManagerPlugin/images/DataTypeTreeWithAssociations.png||GHIDRA||||END|
src/main/help/help/topics/DataTypeManagerPlugin/images/DisassociateDialog.png||GHIDRA||||END|
src/main/help/help/topics/DataTypeManagerPlugin/images/EditPaths.png||GHIDRA||||END|
src/main/help/help/topics/DataTypeManagerPlugin/images/FavoriteDts.png||GHIDRA||||END|
@@ -361,6 +362,7 @@ src/main/help/help/topics/DataTypeManagerPlugin/images/MergeErrorDialog.png||GHI
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|
+src/main/help/help/topics/DataTypeManagerPlugin/images/StructureMergeDialog.png||GHIDRA||||END|
src/main/help/help/topics/DataTypeManagerPlugin/images/UpdateDialog.png||GHIDRA||||END|
src/main/help/help/topics/DataTypeManagerPlugin/images/lockoverlay.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END|
src/main/help/help/topics/DataTypePreviewPlugin/DataTypePreviewPlugin.html||GHIDRA||||END|
diff --git a/Ghidra/Features/Base/data/base.icons.theme.properties b/Ghidra/Features/Base/data/base.icons.theme.properties
index 98e7689a3a..729e6b709b 100644
--- a/Ghidra/Features/Base/data/base.icons.theme.properties
+++ b/Ghidra/Features/Base/data/base.icons.theme.properties
@@ -402,7 +402,7 @@ icon.base.mem.search.panel.scan = view_bottom.png
icon.base.mem.search.panel.search = view_top_bottom.png
icon.base.plugin.quickfix.done = icon.checkmark.green
-
+icon.base.merge.struct.apply = icon.checkmark.green[size(12,12)]
[Dark Defaults]
diff --git a/Ghidra/Features/Base/data/base.theme.properties b/Ghidra/Features/Base/data/base.theme.properties
index f77102a815..f461cc6537 100644
--- a/Ghidra/Features/Base/data/base.theme.properties
+++ b/Ghidra/Features/Base/data/base.theme.properties
@@ -169,7 +169,7 @@ color.bg.plugin.overview.entropy.palette.base.high = color.palette.white
color.bg.plugin.references.table.active.operand = color.palette.lightgray
-color.bg.plugin.register.marker = color.palette.darkcyan
+color.bg.plugin.register.marker = color.palette.darkcyan
color.bg.plugin.windowlocation = color.palette.black
color.bg.plugin.windowlocation.bounds.virtual = color.palette.red
@@ -178,6 +178,8 @@ color.bg.plugin.windowlocation.screens = color.palette.orange
color.bg.plugin.windowlocation.window.selected = color.palette.lime
color.fg.plugin.windowlocation.window.text = color.palette.gray
+color.bg.plugin.struct.merge.selection.non.focused = rgb(211, 211, 211)
+
font.print = SansSerif-PLAIN-10
font.splash.infopanel = SansSerif-BOLD-14
@@ -215,5 +217,6 @@ font.textarea.astextfield = [laf.font]TextField.font
color.bg.undefined = #3A2A48
color.fg.analysis.options.prototype.selected = color.palette.crimson
+color.bg.plugin.struct.merge.selection.non.focused = rgb(15, 42, 61)
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 8872548c81..c0c7e426d1 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
@@ -1095,22 +1095,70 @@
{
+ // the max number of display columns any ComparisonItem can have
+ public static int MAX_COLS = 5;
+
+ enum ItemApplyState {
+ NON_APPLIALBLE, APPLIED, NOT_APPLIED
+ }
+
+ private int line;
+ private String type;
+
+ /**
+ * Constructor
+ * @param type The type of comparison item (name, comment, component, etc.)
+ * @param line the line number in the overall coordinated structure model
+ */
+ ComparisonItem(String type, int line) {
+ this.type = type;
+ this.line = line;
+ }
+
+ /**
+ * Returns the text to be display for the given column index.
+ * @param column the index of the column to get text for
+ * @return the text to be display for the given column index.
+ */
+ public String getColumnText(int column) {
+ return "";
+ }
+
+ /**
+ * Returns true if this items represents something that can be applied or not applied. Used
+ * to determine if a button should be created for this item. For
+ * example, a structure name can applied from either side, but the simple syntax line "{"
+ * is the same for all sides, so it is never appliable and should not have a corresponding
+ * button.
+ * @return true if this item can potentially be applied.
+ */
+ public boolean isAppliable() {
+ return false;
+ }
+
+ /**
+ * Returns true if any information in this item is not currently applied to the merged item.
+ * if true, the button should be displayed as unselected, indicating to the user that this
+ * item has information that can be applied.
+ * @return true if any information in this item can be applied.
+ */
+ public boolean canApplyAny() {
+ return false;
+ }
+
+ /**
+ * Returns true if the information from this item can be cleared. Currently only items
+ * from component lines can be cleared. Items such as the structure name can never be cleared
+ * and can only change by selecting the other side value.
+ * If true, the button will be allowed to become unselected without the other side being
+ * selected.
+ * @return true if the information from this item can be cleared
+ */
+ public boolean canClear() {
+ return false;
+ }
+
+ /**
+ * Returns true if the specific information represented by the given column index is applied.
+ * This is used by the renderer to bold information that is applied and fade information
+ * that is not applied.
+ * @param column the column index to check if it is applied
+ * @return true if this column information is currently applied
+ */
+ public boolean isApplied(int column) {
+ return false;
+ }
+
+ /**
+ * Returns true if the specific information represented by the given column index is something
+ * can be applied whether or not it is currently applied. This is used by the renderer to
+ * render this columns test normally (not faded or bold)
+ * @param column the column index to check if it is appliable
+ * @return true if this columns information is changeable
+ */
+ public boolean isAppliable(int column) {
+ return false;
+ }
+
+ /**
+ * Returns the minimum width of this column. Used to reserve space for a column even when
+ * there is no text to display in the column. The column may be wider if its text is wider
+ * than the minimum width. Used to help the renderer allocate available extra space when
+ * the view is resized.
+ * @param column the column to get the min width for
+ * @return the minimum width of this column
+ */
+ public int getMinWidth(int column) {
+ return 0;
+ }
+
+ /**
+ * Specifies if the column text should be left or right justified within it column.
+ * @param column the column index
+ * @return true if the column text should be justified to the left side of the column
+ */
+ public boolean isLeftJustified(int column) {
+ return true;
+ }
+
+ /**
+ * Applies all the information in this item to the merged structure.
+ */
+ public void applyAll() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Clears this item from the merged structure. Normally items from one side or the other
+ * are cleared when the corresponding item for the other side is applied. This allows the
+ * state where neither side is applied.
+ */
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Returns if this item represent a blank line. Useful for removing blank lines from the
+ * merge structure view.
+ * @return true if this item represents a blank line
+ */
+ public boolean isBlank() {
+ return false;
+ }
+
+ /**
+ * Return the line number for this item in the coordinated display. Note that this may
+ * be different from its index in its list model. The merged display removes blank lines, but
+ * maintains the line number where it matches in the left/right displays. This is uses to
+ * coordinated the left/right/merged views.
+ * @return the line number for this item
+ */
+ public int getLine() {
+ return line;
+ }
+
+ /**
+ * Returns the type for this item. Used to align the columns of like items. For example,
+ * all the fields in the component lines must line up, but that alignment is not coordinated
+ * with other categories such as the structure name line.
+ * @return the category for this item.
+ */
+ protected String getType() {
+ return type;
+ }
+
+ @Override
+ public int compareTo(ComparisonItem o) {
+ return line - o.line;
+ }
+}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/ComparisonItemLayout.java b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/ComparisonItemLayout.java
new file mode 100644
index 0000000000..167ba8d360
--- /dev/null
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/ComparisonItemLayout.java
@@ -0,0 +1,149 @@
+/* ###
+ * 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.merge.structures;
+
+import java.awt.*;
+
+/**
+ * LayoutManager for arranging the labels for each column in a {@link ComparisonItem}.
+ * The main idea here is that each type of item has a set of min/max widths associated with
+ * each column that is used to align like types of items so that their fields line up.
+ * The tricky part is how to handle sizing them as the view is expanded or contracted.
+ * Initially, all columns are given their minimum width and if the total is greater then the
+ * available width, the last columns are clipped. If the available width is greater than the
+ * sum of the minimum widths, the extra width (10 at a time) is given to each column that still has
+ * text wider than its current width. This is repeated until the extra width is used up or all
+ * columns have all the width they need to display their text.
+ *
+ */
+public class ComparisonItemLayout implements LayoutManager {
+ private static int HGAP = 5;
+ private FontMetrics metrics;
+
+ private ColumnWidths minMaxWidths = new ColumnWidths();
+ private int[] adjustedWidths = new int[ComparisonItem.MAX_COLS];
+
+ @Override
+ public void addLayoutComponent(String name, Component comp) {
+ // nothing to do
+ }
+
+ @Override
+ public void removeLayoutComponent(Component comp) {
+ // nothing to do
+ }
+
+ @Override
+ public Dimension preferredLayoutSize(Container parent) {
+ return minimumLayoutSize(parent);
+ }
+
+ @Override
+ public Dimension minimumLayoutSize(Container parent) {
+ int n = parent.getComponentCount();
+ int width = 0;
+ for (int i = 0; i < n; i++) {
+ width += minMaxWidths.getMinWidth(i);
+ }
+ Insets insets = parent.getInsets();
+ return new Dimension(width + insets.left + insets.right + 3 * HGAP, 0);
+ }
+
+ @Override
+ public void layoutContainer(Container parent) {
+ int n = parent.getComponentCount();
+ Dimension d = parent.getSize();
+ Insets insets = parent.getInsets();
+ int width = d.width - insets.left - insets.right - 3 * HGAP;
+ int height = d.height - insets.top - insets.bottom;
+ computeWidths(width, n);
+
+ int x = 0;
+ int widthSoFar = 0;
+ for (int i = 0; i < n; i++) {
+ int compWidth = Math.min(adjustedWidths[i], width - widthSoFar);
+ Component c = parent.getComponent(i);
+ c.setBounds(x, 0, compWidth, height);
+ x += compWidth + HGAP;
+ widthSoFar += compWidth;
+ }
+
+ }
+
+ private void computeWidths(int width, int componentCount) {
+ int totalWidth = 0;
+ int totalMaxWidth = 0;
+ for (int i = 0; i < componentCount; i++) {
+ int min = minMaxWidths.getMinWidth(i);
+ int max = minMaxWidths.getMaxWidth(i);
+ totalWidth += min;
+ totalMaxWidth += max;
+ adjustedWidths[i] = min; // initialize columns widths to min size
+ }
+
+ if (width >= totalMaxWidth) {
+ // set all columns to max
+ for (int i = 0; i < componentCount; i++) {
+ adjustedWidths[i] = minMaxWidths.getMaxWidth(i);
+ }
+ return;
+ }
+ // otherwise distribute extra width to those columns currently less than their max
+ while (totalWidth < width && totalWidth < totalMaxWidth) {
+ totalWidth = addToColumnWidths(totalMaxWidth - totalWidth, componentCount, metrics);
+ }
+ }
+
+ private int addToColumnWidths(int extraWidth, int componentCount, FontMetrics metrics) {
+ int totalWidth = 0;
+ for (int i = 0; i < ComparisonItem.MAX_COLS; i++) {
+ int maxWidth = minMaxWidths.getMaxWidth(i);
+ int incrementAmount = Math.min(maxWidth - adjustedWidths[i], 10);
+ adjustedWidths[i] += incrementAmount;
+ extraWidth -= incrementAmount;
+ totalWidth += adjustedWidths[i];
+ if (extraWidth <= 0) {
+ break;
+ }
+ }
+ return totalWidth;
+ }
+
+ public void setColumnWidths(ColumnWidths widths) {
+ this.minMaxWidths = widths;
+ }
+
+ static class ColumnWidths {
+ private int[] minWidths = new int[ComparisonItem.MAX_COLS];
+ private int[] maxWidths = new int[ComparisonItem.MAX_COLS];
+
+ int getMinWidth(int column) {
+ return minWidths[column];
+ }
+
+ int getMaxWidth(int column) {
+ return maxWidths[column];
+ }
+
+ void addMinWidth(int column, int width) {
+ minWidths[column] = Math.max(minWidths[column], width);
+ }
+
+ void addMaxWidth(int column, int width) {
+ maxWidths[column] = Math.max(maxWidths[column], width);
+ }
+ }
+}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/ComparisonItemRenderer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/ComparisonItemRenderer.java
new file mode 100644
index 0000000000..248512c3ef
--- /dev/null
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/ComparisonItemRenderer.java
@@ -0,0 +1,191 @@
+/* ###
+ * 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.merge.structures;
+
+import static javax.swing.SwingConstants.*;
+
+import java.awt.*;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.*;
+import javax.swing.event.ListDataEvent;
+import javax.swing.event.ListDataListener;
+
+import generic.theme.GColor;
+import generic.theme.GThemeDefaults.Colors;
+import ghidra.app.merge.structures.ComparisonItemLayout.ColumnWidths;
+
+/**
+ * ListCellRenderer for rendering structure lines in {@link CoordinatedStructureDisplay}. It
+ * consists of a label for each possible column. The labels are arranged by a
+ * {@link ComparisonItemLayout} which is fed column widths for each type of line item. The
+ * columns widths for each type are computed when the renderer is constructed by examining all
+ * the lines in the model and computing the min/max column widths for that type.
+ */
+public class ComparisonItemRenderer implements ListCellRenderer, ListDataListener {
+ private static Color FADED_COLOR = Colors.FOREGROUND_DISABLED;
+ private static Color NON_FOCUSED_SELECTION_BG_COLOR =
+ new GColor("color.bg.plugin.struct.merge.selection.non.focused");
+
+ private JPanel panel;
+ private JLabel[] labels = new JLabel[ComparisonItem.MAX_COLS];
+ private int lineHeight;
+ private ComparisonItemLayout layout;
+ private Font normal;
+ private Font bold;
+
+ private FontMetrics metrics;
+
+ private Map widthsMap;
+
+ ComparisonItemRenderer(ListModel listModel) {
+ panel = new JPanel();
+ for (int i = 0; i < ComparisonItem.MAX_COLS; i++) {
+ labels[i] = new JLabel();
+ labels[i].setHorizontalAlignment(SwingConstants.LEFT);
+ panel.add(labels[i]);
+ }
+ normal = labels[0].getFont();
+ bold = normal.deriveFont(Font.BOLD);
+ metrics = labels[0].getFontMetrics(bold);
+ lineHeight = metrics.getHeight();
+ layout = new ComparisonItemLayout();
+ panel.setLayout(layout);
+ computeColumnWidths(listModel);
+ listModel.addListDataListener(this);
+ }
+
+ private void computeColumnWidths(ListModel listModel) {
+ widthsMap = new HashMap<>();
+ for (int i = 0; i < listModel.getSize(); i++) {
+ ComparisonItem item = listModel.getElementAt(i);
+ ColumnWidths widths =
+ widthsMap.computeIfAbsent(item.getType(), k -> new ColumnWidths());
+ for (int col = 0; col < ComparisonItem.MAX_COLS; col++) {
+ int maxWidth = metrics.stringWidth(item.getColumnText(col));
+ int minWidth = item.getMinWidth(col);
+ if (minWidth < 0) {
+ minWidth = maxWidth;
+ }
+ widths.addMinWidth(col, minWidth);
+ widths.addMaxWidth(col, maxWidth);
+ }
+ }
+ }
+
+ public FontMetrics getFontMetrics() {
+ return metrics;
+ }
+
+ public int getPreferredHeight() {
+ return lineHeight;
+ }
+
+ @Override
+ public Component getListCellRendererComponent(JList extends ComparisonItem> list,
+ ComparisonItem value, int index, boolean isSelected, boolean cellHasFocus) {
+ layout.setColumnWidths(widthsMap.get(value.getType()));
+ boolean hasFocus = list.hasFocus();
+ // Note: Setting the accessible description on the renderer works and it
+ // reports the correct information as you hover or select lines in the list
+ panel.getAccessibleContext().setAccessibleName(value.getType() + " line");
+ panel.getAccessibleContext().setAccessibleDescription(getAccessibleDescription(value));
+ Color bgColor = getBackgroundColor(list, isSelected, hasFocus);
+ Color fgColor = getForegroundColor(list, isSelected, hasFocus);
+ Color fadedColor = getFadedColor(fgColor, isSelected, hasFocus);
+ for (int i = 0; i < ComparisonItem.MAX_COLS; i++) {
+ labels[i].setText(value.getColumnText(i));
+ labels[i].setHorizontalAlignment(value.isLeftJustified(i) ? LEFT : RIGHT);
+ if (!value.isAppliable(i)) {
+ labels[i].setFont(normal);
+ labels[i].setForeground(fgColor);
+ }
+ else if (value.isApplied(i)) {
+ labels[i].setFont(bold);
+ labels[i].setForeground(fgColor);
+ }
+ else {
+ labels[i].setFont(normal);
+ labels[i].setForeground(fadedColor);
+ }
+ }
+
+ panel.setBackground(bgColor);
+ return panel;
+ }
+
+ private Color getBackgroundColor(JList extends ComparisonItem> list, boolean isSelected,
+ boolean hasFocus) {
+ if (!isSelected) {
+ return list.getBackground();
+ }
+ if (hasFocus) {
+ return list.getSelectionBackground();
+ }
+ return NON_FOCUSED_SELECTION_BG_COLOR;
+ }
+
+ private Color getForegroundColor(JList extends ComparisonItem> list, boolean isSelected,
+ boolean hasFocus) {
+ if (hasFocus && isSelected) {
+ return list.getSelectionForeground();
+ }
+ return list.getForeground();
+ }
+
+ private String getAccessibleDescription(ComparisonItem value) {
+ if (value.isBlank()) {
+ return "Blank line";
+ }
+ StringBuilder builder = new StringBuilder();
+ builder.append(value.toString());
+ if (!value.isAppliable()) {
+ builder.append(" Status: Not Appliable");
+ }
+ else if (value.canApplyAny()) {
+ builder.append(" Status: Not Applied");
+ }
+ else {
+ builder.append(" Status: Applied");
+ }
+ return builder.toString();
+ }
+
+ private Color getFadedColor(Color fgColor, boolean isSelected, boolean hasFocus) {
+ if (isSelected && !hasFocus) {
+ return fgColor;
+ }
+ return FADED_COLOR;
+ }
+
+ @Override
+ public void intervalAdded(ListDataEvent e) {
+ // don't care
+ }
+
+ @Override
+ public void intervalRemoved(ListDataEvent e) {
+ // don't care
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void contentsChanged(ListDataEvent e) {
+ computeColumnWidths((ListModel) e.getSource());
+ }
+
+}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/CoordinatedStructureDisplay.java b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/CoordinatedStructureDisplay.java
new file mode 100644
index 0000000000..f8de51b633
--- /dev/null
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/CoordinatedStructureDisplay.java
@@ -0,0 +1,231 @@
+/* ###
+ * 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.merge.structures;
+
+import java.awt.*;
+
+import javax.swing.*;
+import javax.swing.border.Border;
+import javax.swing.event.ChangeListener;
+
+import docking.actions.KeyBindingUtils;
+import generic.theme.GIcon;
+
+/**
+ * Class for displaying a view into a {@link CoordinatedStructureModel}, showing either the
+ * left structure, the right structure, or the merged structure. It consists of a JList in
+ * a JScrollpane where the list model is extracted from the {@link CoordinatedStructureModel} for
+ * either the left,right, or merged view. These views track together for both view scrolling and
+ * list selection. They all share a {@link DisplayCoordinator} that assists with coordinating the
+ * views.
+ */
+public class CoordinatedStructureDisplay extends JPanel {
+ public static final int MARGIN = 10;
+
+ static Icon APPLY_ICON = new GIcon("icon.base.merge.struct.apply");
+
+ private JList jList;
+ private JScrollPane scroll;
+ private JScrollBar horizontalScrollbar;
+ private JScrollBar verticalScrollbar;
+
+ private String title;
+ private StructDisplayModel listModel;
+ private int rowHeight;
+
+ private ComparisonItemRenderer renderer;
+
+ private DisplayCoordinator coordinator;
+
+ public CoordinatedStructureDisplay(String title, StructDisplayModel listModel,
+ DisplayCoordinator coordinator) {
+ super(new BorderLayout());
+ this.title = title;
+ this.listModel = listModel;
+ this.coordinator = coordinator;
+ Border emptyBorder = BorderFactory.createEmptyBorder(10, 0, 0, 0);
+ setBorder(BorderFactory.createTitledBorder(emptyBorder, title));
+ renderer = new ComparisonItemRenderer(listModel);
+ jList = new JList(listModel);
+ rowHeight = Math.max(renderer.getPreferredHeight(), APPLY_ICON.getIconHeight() + 6);
+ jList.setFixedCellHeight(rowHeight);
+ jList.setCellRenderer(renderer);
+ jList.setBorder(
+ BorderFactory.createEmptyBorder(MARGIN, MARGIN, MARGIN, MARGIN));
+ jList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+
+ // remove key bindings so we can assign them to our actions
+ clearBinding("SPACE");
+ clearBinding("LEFT");
+ clearBinding("RIGHT");
+
+ scroll = new JScrollPane(jList);
+ horizontalScrollbar = scroll.getHorizontalScrollBar();
+ verticalScrollbar = scroll.getVerticalScrollBar();
+ add(scroll);
+ jList.addListSelectionListener(e -> notifyCoordiatorSelectionChanged());
+ horizontalScrollbar
+ .addAdjustmentListener(e -> coordinator.notifyHorizontalScrollChanged(this, e));
+ verticalScrollbar
+ .addAdjustmentListener(e -> coordinator.notifyVerticalScrollChanged(this, e));
+ coordinator.registerDisplay(this);
+ }
+
+ @Override
+ public String toString() {
+ return title;
+ }
+
+ /**
+ * Sets the list item to be selected based on the the selection change of the given display.
+ * @param changedDisplay the display that was changed by the user and is being used to update
+ * the other displays.
+ * @param index the list index that was selected in the given display.
+ * @param item the comparison item that was selected the the given display.
+ */
+ void setSelectedItem(CoordinatedStructureDisplay changedDisplay, int index,
+ ComparisonItem item) {
+ if (changedDisplay == this) {
+ return;
+ }
+
+ // If the models have different sizes, find the index of the corresponding item using
+ // the line number from the item. Otherwise, the list index can be used directly.
+ if (changedDisplay.getItemCount() != getItemCount()) {
+ index = listModel.getIndex(item);
+ }
+
+ if (index < 0) {
+ jList.clearSelection();
+ }
+ else {
+ jList.setSelectedIndex(index);
+ }
+ }
+
+ /**
+ * Sets the horizontal scroll position based on the view change in one of the other displays.
+ * @param changedDisplay the display that was changed by the user.
+ * @param value the horizontal scroll position of the given display.
+ */
+ void setHorizontalScroll(CoordinatedStructureDisplay changedDisplay, int value) {
+ if (changedDisplay == this) {
+ return;
+ }
+ horizontalScrollbar.setValue(value);
+ }
+
+ /**
+ * Sets the vertical scroll position based on the view change in one of the other displays.
+ * @param changedDisplay the display that was changed by the user. Coordinating the vertical
+ * position can be tricky because the merged display has had its blank lines removed. If the
+ * model sizes are different, it uses the line number information in the displayed items to
+ * compute the corresponding position.
+ * @param value the vertical scroll position of the given display.
+ */
+ void setVerticalScroll(CoordinatedStructureDisplay changedDisplay, int value) {
+ if (this == changedDisplay) {
+ return;
+ }
+ if (getItemCount() == changedDisplay.getItemCount()) {
+ // If the models are the same size, we can use the vertical scroll position directly
+ verticalScrollbar.setValue(value);
+ }
+ else {
+ // Otherwise, we have to find the corresponding first object in the other display.
+ int idx = changedDisplay.getFirstVisibleIndex();
+ if (idx < 0) {
+ return;
+ }
+ Rectangle r = changedDisplay.jList.getCellBounds(idx, idx);
+ ComparisonItem first = changedDisplay.getFirstVisibleItem();
+
+ // We also compute a vertical line offset so that if the first item has a match and
+ // is partially scrolled off the screen, we can partially scroll this display to match.
+ int offset = r.y - value;
+
+ int index = listModel.getIndex(first);
+ if (index < 0) {
+ offset = 0; // don't partial scroll for inexact match
+ index = -index - 1;
+ }
+ Rectangle cellBounds = jList.getCellBounds(index, index);
+ verticalScrollbar.setValue(cellBounds.y - offset);
+ }
+
+ }
+
+ ComparisonItem getItem(int index) {
+ return listModel.getElementAt(index);
+ }
+
+ int getRowHeight() {
+ return rowHeight;
+ }
+
+ void setRowHeight(int rowHeight) {
+ this.rowHeight = rowHeight;
+ jList.setFixedCellHeight(rowHeight);
+ }
+
+ int getFirstVisibleIndex() {
+ return jList.getFirstVisibleIndex();
+ }
+
+ int getLastVisibleIndex() {
+ return jList.getLastVisibleIndex();
+ }
+
+ ComparisonItem getSelectedItem() {
+ return jList.getSelectedValue();
+ }
+
+ Component getList() {
+ return jList;
+ }
+
+ void addViewportListener(ChangeListener l) {
+ scroll.getViewport().addChangeListener(l);
+ }
+
+ private int getItemCount() {
+ return listModel.getSize();
+ }
+
+ private void clearBinding(String keyName) {
+ KeyBindingUtils.clearKeyBinding(jList, KeyBindingUtils.parseKeyStroke(keyName));
+ KeyBindingUtils.clearKeyBinding(jList, KeyBindingUtils.parseKeyStroke("ctrl " + keyName));
+ KeyBindingUtils.clearKeyBinding(jList, KeyBindingUtils.parseKeyStroke("shift " + keyName));
+ KeyBindingUtils.clearKeyBinding(jList,
+ KeyBindingUtils.parseKeyStroke("ctrl shift " + keyName));
+ }
+
+ private void notifyCoordiatorSelectionChanged() {
+ int selectedIndex = jList.getSelectedIndex();
+ ComparisonItem item = jList.getSelectedValue();
+ coordinator.notifySelectionChanged(this, selectedIndex, item);
+
+ }
+
+ private ComparisonItem getFirstVisibleItem() {
+ int index = jList.getFirstVisibleIndex();
+ if (index >= 0) {
+ return listModel.getElementAt(index);
+ }
+ return null;
+ }
+
+}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/CoordinatedStructureLine.java b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/CoordinatedStructureLine.java
new file mode 100644
index 0000000000..a5fe5a745a
--- /dev/null
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/CoordinatedStructureLine.java
@@ -0,0 +1,83 @@
+/* ###
+ * 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.merge.structures;
+
+import java.util.Objects;
+
+/**
+ * Base class for coordinating display lines of a left, right, and merged structure.
+ */
+public abstract class CoordinatedStructureLine {
+ protected ComparisonItem left;
+ protected ComparisonItem right;
+ protected ComparisonItem merged;
+ protected CoordinatedStructureModel model;
+
+ public enum CompareId {
+ LEFT, RIGHT, MERGED;
+ }
+
+ CoordinatedStructureLine(CoordinatedStructureModel model) {
+ this.model = model;
+ }
+
+ /**
+ * Returns either the left, right, or merged comparison item for this line.
+ * @param id the id for which comparison item to return
+ * @return either the left, right, or merged comparison item for this line
+ */
+ ComparisonItem getComparisonItem(CompareId id) {
+ switch (id) {
+ case LEFT:
+ return left;
+ case RIGHT:
+ return right;
+ case MERGED:
+ default:
+ return merged;
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ CoordinatedStructureLine other = (CoordinatedStructureLine) obj;
+ return Objects.equals(left, other.left) &&
+ Objects.equals(right, other.right) &&
+ Objects.equals(merged, other.merged);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(left, right, merged);
+ }
+
+ protected void modelChanged() {
+ model.rebuild();
+ }
+
+ protected void error(String errorMessage) {
+ model.error(errorMessage);
+ }
+}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/CoordinatedStructureModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/CoordinatedStructureModel.java
new file mode 100644
index 0000000000..a177baf96f
--- /dev/null
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/CoordinatedStructureModel.java
@@ -0,0 +1,474 @@
+/* ###
+ * 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.merge.structures;
+
+import java.util.*;
+import java.util.function.Consumer;
+
+import org.apache.commons.lang3.StringUtils;
+
+import ghidra.app.merge.structures.CoordinatedStructureLine.CompareId;
+import ghidra.program.database.data.merge.DataTypeMergeException;
+import ghidra.program.database.data.merge.StructureMerger;
+import ghidra.program.model.data.*;
+import util.CollectionUtils;
+import utility.function.Callback;
+
+/**
+ * Model for merging two structures in an interactive dialog. The first structure will be considered
+ * the left structure (it will be displayed on the left side) and the second structure will be
+ * considered the right structure. This class will internally generate a merged structure by
+ * combining the two given structures. Initially, if there is a conflict, the first structure (left)
+ * will be given precedence.
+ */
+public class CoordinatedStructureModel {
+
+ private Structure leftStruct;
+ private Structure rightStruct;
+ private Structure mergedStruct;
+ private List compareLines;
+ private Consumer errorHandler;
+ private List changeCallbacks = new ArrayList<>();
+
+ /**
+ * Constructor
+ * @param struct1 the left structure (has initial precedence for conflicts)
+ * @param struct2 the right structure
+ * @param errorHandler a consumer for reporting errors
+ */
+ public CoordinatedStructureModel(Structure struct1, Structure struct2,
+ Consumer errorHandler) {
+ this.leftStruct = struct1;
+ this.errorHandler = errorHandler;
+
+ // make sure struct2 has same data organization as struct1
+ this.rightStruct = struct2.clone(struct1.getDataTypeManager());
+ StructureMerger merger = new StructureMerger(struct1, struct2, false);
+ try {
+ this.mergedStruct = merger.merge();
+ }
+ catch (DataTypeMergeException e) {
+ this.mergedStruct = (Structure) struct1.copy(struct1.getDataTypeManager());
+ }
+
+ compareLines = buildLines();
+ }
+
+ /**
+ * Completely rebuilds all the structure compare lines. Called whenever any change is made to
+ * the merged structure.
+ */
+ void rebuild() {
+ compareLines = buildLines();
+ for (Callback callback : changeCallbacks) {
+ callback.call();
+ }
+ }
+
+ private List buildLines() {
+ List list = new ArrayList<>();
+ LineBuilder lineBuilder = new LineBuilder(list);
+
+ String description1 = leftStruct.getDescription();
+ String description2 = rightStruct.getDescription();
+ if (!StringUtils.isBlank(description1) || !StringUtils.isBlank(description2)) {
+ list.add(new StructureDescriptionLine(this, leftStruct, rightStruct, mergedStruct,
+ list.size()));
+ }
+
+ list.add(new StructureNameLine(this, leftStruct, rightStruct, mergedStruct, list.size()));
+ list.add(new StructureInfoLine(this, getInfo(leftStruct), getInfo(rightStruct),
+ getInfo(mergedStruct), list.size(), "Structure properties"));
+ list.add(new StructureInfoLine(this, "{", list.size(), "Syntax"));
+
+ lineBuilder.addComponentLines();
+
+ list.add(new StructureInfoLine(this, "}", list.size(), "Syntax"));
+ return list;
+ }
+
+ private String getInfo(Structure s) {
+ StringBuilder buf = new StringBuilder();
+ buf.append("Length = ");
+ int length = s.getLength();
+ buf.append(length);
+ buf.append(" (0x");
+ buf.append(Integer.toString(length, 16));
+ buf.append("), alignment = ");
+ buf.append(s.getAlignment());
+ buf.append(", Packed = ");
+ buf.append(s.isPackingEnabled());
+ return buf.toString();
+ }
+
+ public int getSize() {
+ return compareLines.size();
+ }
+
+ /**
+ * Inner class to build the coordinated component lines (the hard part) of the three structures.
+ */
+ private class LineBuilder {
+ private List lines;
+ private StructureComponentLine lastComponentLine;
+ private DefinedComponentQueue q1 = new DefinedComponentQueue(leftStruct);
+ private DefinedComponentQueue q2 = new DefinedComponentQueue(rightStruct);
+ private DefinedComponentQueue q3 = new DefinedComponentQueue(mergedStruct);
+
+ LineBuilder(List lines) {
+ this.lines = lines;
+ }
+
+ void addComponentLines() {
+ int offset = getNextOffset();
+ while (offset >= 0) {
+ processOffset(offset);
+ offset = getNextOffset();
+ }
+ fillGaps(getMaxSize());
+ }
+
+ private int getMaxSize() {
+ int size = Math.max(leftStruct.getLength(), rightStruct.getLength());
+ return Math.max(size, mergedStruct.getLength());
+ }
+
+ private int getNextOffset() {
+ int offset1 = q1.nextOffset();
+ int offset2 = q2.nextOffset();
+ int offset3 = q3.nextOffset();
+
+ int offset = getMinOffset(offset1, offset2);
+ return getMinOffset(offset, offset3);
+ }
+
+ private int getMinOffset(int offset1, int offset2) {
+ if (offset1 < 0) {
+ return offset2;
+ }
+ if (offset2 < 0) {
+ return offset1;
+ }
+ return Math.min(offset1, offset2);
+ }
+
+ private void processOffset(int offset) {
+ fillGaps(offset);
+
+ if (q1.hasZeroComp(offset) || q2.hasZeroComp(offset) || q3.hasZeroComp(offset)) {
+ processZeroLengthComponents(offset);
+ }
+
+ if (q1.hasBitField(offset) || q2.hasBitField(offset) || q3.hasBitField(offset)) {
+ processBitFields(offset);
+ }
+
+ DataTypeComponent comp1 = q1.nextOffset() == offset ? q1.next() : null;
+ DataTypeComponent comp2 = q2.nextOffset() == offset ? q2.next() : null;
+ DataTypeComponent comp3 = q3.nextOffset() == offset ? q3.next() : null;
+ if (CollectionUtils.isAllNull(comp1, comp2, comp3)) {
+ return;
+ }
+
+ if (comp1 == null) {
+ // we already know that a defined component is not there, but this checks for
+ // an undefined at that offset
+ comp1 = getUndefinedComp(leftStruct, offset);
+ }
+ if (comp2 == null) {
+ comp2 = getUndefinedComp(rightStruct, offset);
+ }
+ if (comp3 == null) {
+ comp3 = getUndefinedComp(mergedStruct, offset);
+ }
+ int length = getMaxLength(comp1, comp2, comp3);
+ lastComponentLine =
+ new StructureComponentLine(CoordinatedStructureModel.this, leftStruct, rightStruct,
+ mergedStruct, comp1, comp2, comp3, offset, length, lines.size());
+ lines.add(lastComponentLine);
+ }
+
+ private void processBitFields(int offset) {
+ boolean isFirstBitFieldLine = true;
+ DataTypeComponent bitField1 = q1.hasBitField(offset) ? q1.next() : null;
+ DataTypeComponent bitField2 = q2.hasBitField(offset) ? q2.next() : null;
+
+ while (bitField1 != null || bitField2 != null) {
+ int result = compareBitFields(bitField1, bitField2, offset);
+ if (result == 0) {
+ addBitFieldLine(bitField1, bitField2, offset, isFirstBitFieldLine);
+ bitField1 = q1.hasBitField(offset) ? q1.next() : null;
+ bitField2 = q2.hasBitField(offset) ? q2.next() : null;
+ }
+ else if (result < 0) {
+ addBitFieldLine(bitField1, null, offset, isFirstBitFieldLine);
+ bitField1 = q1.hasBitField(offset) ? q1.next() : null;
+ }
+ else {
+ addBitFieldLine(null, bitField2, offset, isFirstBitFieldLine);
+ bitField2 = q2.hasBitField(offset) ? q2.next() : null;
+ }
+ isFirstBitFieldLine = false;
+ }
+ }
+
+ private void addBitFieldLine(DataTypeComponent leftComp, DataTypeComponent rightComp,
+ int offset, boolean isFirstBitFieldLineForOffset) {
+ DataTypeComponent mergedComp = null;
+ DataTypeComponent comp = leftComp != null ? leftComp : rightComp;
+ int bitFieldLength1 = leftComp != null ? leftComp.getLength() : 0;
+ int bitFieldLength2 = rightComp != null ? rightComp.getLength() : 0;
+ int length = Math.max(bitFieldLength1, bitFieldLength2);
+
+ if (q3.hasBitField(offset)) {
+ // peek at the next component in the result struct and see if it is a bitfield
+ // that matches the entry we are creating
+ DataTypeComponent peek = q3.peek();
+ if (compareBitFields(comp, peek, offset) == 0) {
+ mergedComp = q3.next();
+ }
+ }
+
+ // If this is the 1st bit field line for an offset, add in undefined components
+ // if appropriate for null components
+ if (isFirstBitFieldLineForOffset) {
+ if (leftComp == null) {
+ leftComp = getUndefinedComp(leftStruct, offset);
+ }
+ if (rightComp == null) {
+ rightComp = getUndefinedComp(rightStruct, offset);
+ }
+ if (mergedComp == null) {
+ mergedComp = getUndefinedComp(mergedStruct, offset);
+ }
+ }
+ lastComponentLine =
+ new StructureComponentLine(CoordinatedStructureModel.this, leftStruct, rightStruct,
+ mergedStruct,
+ leftComp, rightComp, mergedComp, offset, length, lines.size());
+ lines.add(lastComponentLine);
+
+ }
+
+ private int compareBitFields(DataTypeComponent bitField1, DataTypeComponent bitField2,
+ int offset) {
+ if (bitField1 == null) {
+ return 1;
+ }
+ if (bitField2 == null) {
+ return -1;
+ }
+ BitFieldDataType bfdt1 = (BitFieldDataType) bitField1.getDataType();
+ BitFieldDataType bfdt2 = (BitFieldDataType) bitField2.getDataType();
+ int bitStart1 = BitFieldDataType.getNormalizedBitOffset(bfdt1, offset);
+ int bitStart2 = BitFieldDataType.getNormalizedBitOffset(bfdt2, offset);
+
+ return bitStart1 - bitStart2;
+ }
+
+ private void processZeroLengthComponents(int offset) {
+ List comps1 = getZeroLengthComps(q1, offset);
+ List comps2 = getZeroLengthComps(q2, offset);
+ List comps3 = getZeroLengthComps(q3, offset);
+
+ for (DataTypeComponent comp1 : comps1) {
+ DataTypeComponent comp2 = findSameComp(comps2, comp1);
+ DataTypeComponent comp3 = findSameComp(comps3, comp1);
+ lastComponentLine =
+ new StructureComponentLine(CoordinatedStructureModel.this, leftStruct,
+ rightStruct, mergedStruct, comp1, comp2, comp3, offset, 0, lines.size());
+ lines.add(lastComponentLine);
+ }
+ for (DataTypeComponent comp2 : comps2) {
+ DataTypeComponent comp3 = findSameComp(comps3, comp2);
+ lastComponentLine =
+ new StructureComponentLine(CoordinatedStructureModel.this, leftStruct,
+ rightStruct, mergedStruct, null, comp2, comp3, offset, 0, lines.size());
+ lines.add(lastComponentLine);
+ }
+ for (DataTypeComponent comp3 : comps3) {
+ lastComponentLine =
+ new StructureComponentLine(CoordinatedStructureModel.this, leftStruct,
+ rightStruct, mergedStruct, null, null, comp3, offset, 0, lines.size());
+ lines.add(lastComponentLine);
+ }
+
+ }
+
+ private DataTypeComponent findSameComp(List list,
+ DataTypeComponent comp) {
+ if (list.isEmpty()) {
+ return null;
+ }
+ DataType dataType = comp.getDataType();
+ String name = comp.getFieldName();
+ Iterator it = list.iterator();
+ while (it.hasNext()) {
+ DataTypeComponent next = it.next();
+ if (next.getDataType().isEquivalent(dataType) &&
+ Objects.equals(name, next.getFieldName())) {
+ it.remove();
+ return next;
+ }
+ }
+ return null;
+ }
+
+ private List getZeroLengthComps(DefinedComponentQueue q, int offset) {
+ if (!q.hasZeroComp(offset)) {
+ return Collections.emptyList();
+ }
+ List list = new ArrayList<>();
+ while (q.hasZeroComp(offset)) {
+ list.add(q.next());
+ }
+ return list;
+ }
+
+ private DataTypeComponent getUndefinedComp(Structure struct, int offset) {
+ DataTypeComponent comp = struct.getComponentAt(offset);
+ if (comp != null && comp.getDataType() == DataType.DEFAULT) {
+ return comp;
+ }
+ return null;
+ }
+
+ private int getMaxLength(DataTypeComponent comp1, DataTypeComponent comp2,
+ DataTypeComponent comp3) {
+ int length = 0;
+ if (comp1 != null) {
+ length = comp1.getLength();
+ }
+ if (comp2 != null) {
+ length = Math.max(length, comp2.getLength());
+ }
+ if (comp3 != null) {
+ length = Math.max(length, comp3.getLength());
+ }
+ return length;
+ }
+
+ private void fillGaps(int nextOffset) {
+ int offset = 0;
+ int length = nextOffset;
+ if (lastComponentLine != null) {
+ offset = lastComponentLine.getOffset() + lastComponentLine.getSize();
+ length = nextOffset - offset;
+ }
+ if (length > 0) {
+ DataTypeComponent comp1 = leftStruct.getComponentAt(offset);
+ DataTypeComponent comp2 = rightStruct.getComponentAt(offset);
+ DataTypeComponent comp3 = mergedStruct.getComponentAt(offset);
+ lines.add(
+ new StructureComponentLine(CoordinatedStructureModel.this, leftStruct,
+ rightStruct,
+ mergedStruct, comp1, comp2, comp3, offset, length, lines.size()));
+ }
+ }
+
+ }
+
+ private class DefinedComponentQueue {
+ private DataTypeComponent[] definedComponents;
+ private int current = 0;
+
+ DefinedComponentQueue(Structure struct) {
+ definedComponents = struct.getDefinedComponents();
+ }
+
+ public boolean hasBitField(int offset) {
+ if (hasNext()) {
+ DataTypeComponent comp = definedComponents[current];
+ if (comp.getOffset() == offset && comp.getDataType() instanceof BitFieldDataType) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean hasZeroComp(int offset) {
+ if (hasNext()) {
+ DataTypeComponent comp = definedComponents[current];
+ return comp.getOffset() == offset && comp.getLength() == 0;
+ }
+ return false;
+ }
+
+ public DataTypeComponent next() {
+ if (current >= definedComponents.length) {
+ return null;
+ }
+ DataTypeComponent comp = definedComponents[current];
+ current++;
+ return comp;
+ }
+
+ public DataTypeComponent peek() {
+ if (current >= definedComponents.length) {
+ return null;
+ }
+ return definedComponents[current];
+ }
+
+ public int nextOffset() {
+ if (hasNext()) {
+ return definedComponents[current].getOffset();
+ }
+ return -1;
+ }
+
+ public boolean hasNext() {
+ return current < definedComponents.length;
+ }
+ }
+
+ public List getLines() {
+ return compareLines;
+ }
+
+ public void addChangeListener(Callback callback) {
+ changeCallbacks.add(callback);
+ }
+
+ public List getData(CompareId compareId) {
+ List list = new ArrayList<>();
+ for (CoordinatedStructureLine line : compareLines) {
+ ComparisonItem item = line.getComparisonItem(compareId);
+ if (compareId == CompareId.MERGED && item.isBlank()) {
+ // skip blank lines in merged structure display
+ continue;
+ }
+ list.add(item);
+ }
+ return list;
+ }
+
+ public CoordinatedStructureLine getLine(int line) {
+ if (line < compareLines.size()) {
+ return compareLines.get(line);
+ }
+ return null;
+ }
+
+ void error(String message) {
+ errorHandler.accept(message);
+ }
+
+ public Structure getMergedStructure() {
+ return mergedStruct;
+ }
+
+}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/DisplayCoordinator.java b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/DisplayCoordinator.java
new file mode 100644
index 0000000000..843dd4d45b
--- /dev/null
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/DisplayCoordinator.java
@@ -0,0 +1,88 @@
+/* ###
+ * 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.merge.structures;
+
+import java.awt.event.AdjustmentEvent;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class for coordinating the scrolling and line selection of the three structure display.
+ */
+class DisplayCoordinator {
+ private List displays = new ArrayList<>();
+ private boolean isChanging;
+
+ void registerDisplay(CoordinatedStructureDisplay display) {
+ displays.add(display);
+ }
+
+ void notifySelectionChanged(CoordinatedStructureDisplay changedDisplay,
+ int selectedIndex, ComparisonItem item) {
+ if (isChanging) {
+ return;
+ }
+ try {
+ isChanging = true;
+ for (CoordinatedStructureDisplay display : displays) {
+ display.setSelectedItem(changedDisplay, selectedIndex, item);
+ }
+ }
+ finally {
+ isChanging = false;
+ }
+
+ }
+
+ void notifyHorizontalScrollChanged(CoordinatedStructureDisplay d, AdjustmentEvent e) {
+ if (isChanging) {
+ return;
+ }
+ try {
+ isChanging = true;
+ for (CoordinatedStructureDisplay display : displays) {
+ if (display != d) {
+ display.setHorizontalScroll(d, e.getValue());
+ }
+ }
+ }
+ finally {
+ isChanging = false;
+ }
+ }
+
+ void notifyVerticalScrollChanged(CoordinatedStructureDisplay d, AdjustmentEvent e) {
+ if (isChanging) {
+ return;
+ }
+ try {
+ isChanging = true;
+ for (CoordinatedStructureDisplay display : displays) {
+ if (display != d) {
+ display.setVerticalScroll(d, e.getValue());
+ }
+ }
+ }
+ finally {
+ isChanging = false;
+ }
+ }
+
+ void setChanging(boolean b) {
+ isChanging = b;
+ }
+
+}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/StructDisplayModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/StructDisplayModel.java
new file mode 100644
index 0000000000..87f6def40c
--- /dev/null
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/StructDisplayModel.java
@@ -0,0 +1,78 @@
+/* ###
+ * 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.merge.structures;
+
+import java.util.Collections;
+import java.util.List;
+
+import javax.swing.AbstractListModel;
+import javax.swing.ListModel;
+
+import ghidra.app.merge.structures.CoordinatedStructureLine.CompareId;
+
+/**
+ * The {@link ListModel} model for one of the three structure displays.
+ */
+class StructDisplayModel extends AbstractListModel {
+
+ private CoordinatedStructureModel model;
+ private CompareId compareId;
+ private List data;
+
+ /**
+ * Constructor
+ * @param model the comparison model that has the coordinated lines for all three structures.
+ * @param compareId the id that says this is either the left, right, or merged list model.
+ * Used to get the appropriate list of comparison items from the
+ * {@link CoordinatedStructureModel}
+ */
+ StructDisplayModel(CoordinatedStructureModel model, CompareId compareId) {
+ this.model = model;
+ this.compareId = compareId;
+ model.addChangeListener(() -> modelChanged());
+ data = model.getData(compareId);
+ }
+
+ private void modelChanged() {
+ data = model.getData(compareId);
+ fireContentsChanged(this, 0, model.getSize());
+ }
+
+ @Override
+ public int getSize() {
+ return data.size();
+ }
+
+ @Override
+ public ComparisonItem getElementAt(int index) {
+ return data.get(index);
+ }
+
+ /**
+ * Gets the list index of the corresponding item in this list model. Uses the line number info
+ * from the given item to find its internal item that has the same line number.
+ * @param item the item to use to find the corresponding item in this model
+ * @return the list index in this model for the item that has the same line number as the given
+ * item or -1 if not such item exits.
+ */
+ int getIndex(ComparisonItem item) {
+ if (item == null) {
+ return -1;
+ }
+ return Collections.binarySearch(data, item);
+ }
+
+}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/StructureComponentLine.java b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/StructureComponentLine.java
new file mode 100644
index 0000000000..8c51fd942d
--- /dev/null
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/StructureComponentLine.java
@@ -0,0 +1,444 @@
+/* ###
+ * 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.merge.structures;
+
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.commons.lang3.StringUtils;
+
+import ghidra.program.model.data.*;
+
+/**
+ * {@link CoordinatedStructureLine} for showing structure components.
+ */
+public class StructureComponentLine extends CoordinatedStructureLine {
+ public static final int COMPONENT_INDENT = 5;
+ public static final int OFFSET_SIZE = 10;
+ public static final int MIN_DT_SIZE = 100;
+ public static final int MIN_NAME_SIZE = 10;
+ public static final int MIN_COMMENT_SIZE = 10;
+ private DataTypeComponent mergedComp;
+ private int offset;
+ private int length;
+ private Structure mergedStruct;
+
+ /**
+ * Constructor
+ * @param model the {@link CoordinatedStructureModel}
+ * @param leftStruct the left structure
+ * @param rightStruct the right structure
+ * @param mergedStruct the merged structure
+ * @param left the left component at the offset (can be null)
+ * @param right the right component at the offset (can be null)
+ * @param merged the merged component at the offset (can be null)
+ * @param offset the offset into the structures for this component line
+ * @param length the size of the this component line (will be the largest of the three)
+ * @param line the line number where this component will be shown in the overall list of
+ * line items (including name, description, info, etc.)
+ */
+ StructureComponentLine(CoordinatedStructureModel model, Structure leftStruct,
+ Structure rightStruct,
+ Structure mergedStruct, DataTypeComponent left, DataTypeComponent right,
+ DataTypeComponent merged, int offset, int length, int line) {
+ super(model);
+ this.mergedStruct = mergedStruct;
+ this.left = new StructureComponentItem(left, right, leftStruct, line);
+ this.right = new StructureComponentItem(right, left, rightStruct, line);
+ this.merged = new StructureComponentItem(merged, null, mergedStruct, line);
+ this.mergedComp = merged;
+ this.offset = offset;
+ this.length = length;
+ }
+
+ int getSize() {
+ return length;
+ }
+
+ int getOffset() {
+ return offset;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ StructureComponentLine other = (StructureComponentLine) obj;
+ return Objects.equals(left, other.left) && length == other.length &&
+ Objects.equals(merged, other.merged) && offset == other.offset &&
+ Objects.equals(right, other.right);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("\nLeft: ");
+ buf.append(left.toString());
+ buf.append("\n");
+ buf.append("Right: ");
+ buf.append(right.toString());
+ buf.append("\n");
+ buf.append("Merged: ");
+ buf.append(merged.toString());
+ buf.append("\n");
+ buf.append("offset = ");
+ buf.append(offset);
+ buf.append(", length = ");
+ buf.append(length);
+ buf.append("\n");
+ return buf.toString();
+ }
+
+ /**
+ * Class for the individual {@link ComparisonItem}s for each of the structures.
+ */
+ private class StructureComponentItem extends ComparisonItem {
+ private static final int OFFSET_COL = 1;
+ private static final int DATATYPE_COL = 2;
+ private static final int NAME_COL = 3;
+ private static final int COMMENT_COL = 4;
+
+ private DataTypeComponent myComp;
+ private DataTypeComponent otherComp;
+ private Structure struct;
+
+ StructureComponentItem(DataTypeComponent comp, DataTypeComponent other, Structure struct,
+ int line) {
+ super("Component", line);
+ this.myComp = comp;
+ this.otherComp = other;
+ this.struct = struct;
+ }
+
+ @Override
+ public String getColumnText(int column) {
+ if (myComp == null) {
+ return "";
+ }
+ switch (column) {
+ case OFFSET_COL:
+ return Integer.toString(offset) + " ";
+ case DATATYPE_COL:
+ DataType dataType = myComp.getDataType();
+ if (dataType == DataType.DEFAULT) {
+ return "undefined (" + getUndefinedLength() + ")";
+ }
+ return getDataTypeDisplayName(dataType);
+ case NAME_COL:
+ String name = myComp.getFieldName();
+ return name == null ? "" : name;
+ case COMMENT_COL:
+ String comment = myComp.getComment();
+ return StringUtils.isBlank(comment) ? "" : "// " + comment;
+ default:
+ return "";
+ }
+ }
+
+ private String getDataTypeDisplayName(DataType dataType) {
+ String name = dataType.getDisplayName();
+ if (dataType instanceof BitFieldDataType bfdt) {
+ int startBit = bfdt.getBitOffset();
+ int endBit = startBit + bfdt.getBitSize() - 1;
+ name += " (%d, %d)".formatted(startBit, endBit);
+ }
+ return name;
+ }
+
+ private int getUndefinedLength() {
+ // loop until we find the next line that has a different offset than this line
+ // The difference will be the undefined length;
+ for (int i = getLine() + 1; i < model.getSize(); i++) {
+ CoordinatedStructureLine compareLine = model.getLine(i);
+ if (!(compareLine instanceof StructureComponentLine componentLine)) {
+ break;
+ }
+ if (componentLine.offset != offset) {
+ return componentLine.offset - offset;
+ }
+ }
+ // otherwise, the length is to the end of the struct
+ return struct.getLength() - offset;
+ }
+
+ @Override
+ public boolean isLeftJustified(int column) {
+ return column != 1;
+ }
+
+ @Override
+ public int getMinWidth(int column) {
+ switch (column) {
+ case 0:
+ return COMPONENT_INDENT;
+ case OFFSET_COL:
+ return OFFSET_SIZE;
+ case DATATYPE_COL:
+ return MIN_DT_SIZE;
+ case NAME_COL:
+ return MIN_NAME_SIZE;
+ case COMMENT_COL:
+ return MIN_COMMENT_SIZE;
+ default:
+ return 0;
+ }
+ }
+
+ @Override
+ public boolean canApplyAny() {
+ if (myComp == mergedComp) {
+ return false;
+ }
+
+ return !(isDatatypeApplied() && isNameApplied() && isCommentApplied());
+ }
+
+ @Override
+ public boolean isAppliable() {
+ if (myComp == null || myComp.getDataType() == DataType.DEFAULT) {
+ return false;
+ }
+ if (otherComp == null) {
+ return true;
+ }
+ return !myComp.isEquivalent(otherComp);
+ }
+
+ @Override
+ public boolean isAppliable(int column) {
+ if (myComp == mergedComp) {
+ return false;
+ }
+ if (myComp == null || myComp == mergedComp ||
+ myComp.getDataType() == DataType.DEFAULT) {
+ return false;
+ }
+ return column == DATATYPE_COL || column == NAME_COL || column == COMMENT_COL;
+ }
+
+ @Override
+ public boolean isApplied(int column) {
+ if (myComp == null || mergedComp == null) {
+ return false;
+ }
+
+ switch (column) {
+ case DATATYPE_COL:
+ return isDatatypeApplied();
+ case NAME_COL:
+ return isNameApplied();
+ case COMMENT_COL:
+ return isNameApplied();
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public boolean canClear() {
+ return mergedComp != null && mergedComp.getDataType() != DataType.DEFAULT;
+ }
+
+ @Override
+ public int hashCode() {
+ return myComp == null ? 0 : myComp.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ StructureComponentItem other = (StructureComponentItem) obj;
+ if (myComp == null) {
+ return other.myComp == null;
+ }
+ if (myComp.getOffset() != other.myComp.getOffset()) {
+ return false;
+ }
+ if (!myComp.getDataType().isEquivalent(other.myComp.getDataType())) {
+ return false;
+ }
+ if (!Objects.equals(myComp.getFieldName(), other.myComp.getFieldName())) {
+ return false;
+ }
+ return Objects.equals(myComp.getComment(), other.myComp.getComment());
+ }
+
+ private boolean isDatatypeApplied() {
+ if (myComp == null || mergedComp == null) {
+ return false;
+ }
+ return myComp.getDataType().isEquivalent(mergedComp.getDataType());
+ }
+
+ private boolean isNameApplied() {
+ if (myComp == null) {
+ return false;
+ }
+
+ if (mergedComp == null) {
+ return false;
+ }
+ return Objects.equals(myComp.getFieldName(), mergedComp.getFieldName());
+ }
+
+ private boolean isCommentApplied() {
+ return Objects.equals(myComp.getComment(), mergedComp.getComment());
+ }
+
+ @Override
+ public String toString() {
+ if (myComp == null) {
+ return "";
+ }
+ StringBuffer buffer = new StringBuffer();
+ buffer.append(myComp.getOffset());
+ buffer.append(" ");
+ buffer.append(getColumnText(DATATYPE_COL));
+
+ String name = myComp.getFieldName();
+ if (name != null) {
+ buffer.append(" " + name);
+ }
+ String comment = myComp.getComment();
+ if (!StringUtils.isBlank(comment)) {
+ buffer.append(" // ");
+ buffer.append(comment);
+ }
+ return buffer.toString();
+ }
+
+ @Override
+ public void applyAll() {
+ if (myComp.getLength() == 0) {
+ mergedStruct.insertAtOffset(offset, myComp.getDataType(), 0, myComp.getFieldName(),
+ myComp.getComment());
+ }
+ else if (myComp.getDataType() instanceof BitFieldDataType bfdt) {
+ makeRoomForBitField(bfdt);
+ DataType dt = bfdt.getBaseDataType();
+ int byteSize = bfdt.getStorageSize();
+ int bitOffset = bfdt.getBitOffset();
+ int bitLength = bfdt.getBitSize();
+ String name = myComp.getFieldName();
+ String comment = myComp.getComment();
+ try {
+ mergedStruct.insertBitFieldAt(offset, byteSize, bitOffset, dt, bitLength, name,
+ comment);
+ }
+ catch (InvalidDataTypeException e) {
+ model.error("Error applying bitfield component at offset " + offset + ": " +
+ e.getMessage());
+ }
+ }
+ else {
+ makeRoom();
+ mergedStruct.replaceAtOffset(offset, myComp.getDataType(), myComp.getLength(),
+ myComp.getFieldName(), myComp.getComment());
+ }
+ modelChanged();
+ }
+
+ private void makeRoomForBitField(BitFieldDataType bfdt) {
+
+ List comps = mergedStruct.getComponentsContaining(offset);
+ for (DataTypeComponent comp : comps) {
+ DataType dt = comp.getDataType();
+ if (dt instanceof BitFieldDataType otherBfdt) {
+ if (BitFieldDataType.intersects(bfdt, otherBfdt, offset, comp.getOffset())) {
+ mergedStruct.clearComponent(comp.getOrdinal());
+ }
+ }
+ else if (dt != DataType.DEFAULT && comp.getOffset() == offset &&
+ comp.getLength() != 0) {
+ mergedStruct.clearComponent(comp.getOrdinal());
+ }
+ }
+
+ for (int i = offset + 1; i < offset + length; i++) {
+ comps = mergedStruct.getComponentsContaining(i);
+ for (DataTypeComponent comp : comps) {
+ // To avoid repeating looking at components we already examined, only look
+ // at components that start at the offset being examined. (We already
+ // handled all the one that extend into the new component, so only need
+ // to worry about the ones that start inside it.)
+ if (comp.getOffset() != i) {
+ continue;
+ }
+ DataType dt = comp.getDataType();
+ if (dt instanceof BitFieldDataType otherBfdt) {
+ if (BitFieldDataType.intersects(bfdt, otherBfdt, offset, i)) {
+ mergedStruct.clearComponent(comp.getOrdinal());
+ }
+ }
+ else {
+ // if we have anything other than a bitfield, then wipe them all out
+ // because only bitfields can coexist with other bitfields.
+ mergedStruct.clearAtOffset(i);
+ return;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void clear() {
+ mergedStruct.clearComponent(mergedComp.getOrdinal());
+ modelChanged();
+ }
+
+ private void makeRoom() {
+ if (myComp.getLength() == 0) {
+ // just make sure there is that starts here
+ if (mergedStruct.getComponentAt(offset) == null) {
+ mergedStruct.clearAtOffset(offset);
+ }
+ return;
+ }
+
+ List list = mergedStruct.getComponentsContaining(offset);
+ for (DataTypeComponent comp : list) {
+ DataType dt = comp.getDataType();
+ if (dt != DataType.DEFAULT && comp.getOffset() == offset && comp.getLength() != 0) {
+ mergedStruct.clearComponent(comp.getOrdinal());
+ }
+ }
+
+ for (int i = offset + 1; i < offset + length; i++) {
+ mergedStruct.clearAtOffset(i);
+ }
+ }
+
+ @Override
+ public boolean isBlank() {
+ return myComp == null;
+ }
+ }
+}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/StructureDescriptionLine.java b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/StructureDescriptionLine.java
new file mode 100644
index 0000000000..94945772d6
--- /dev/null
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/StructureDescriptionLine.java
@@ -0,0 +1,158 @@
+/* ###
+ * 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.merge.structures;
+
+import java.util.Objects;
+
+import org.apache.commons.lang3.StringUtils;
+
+import ghidra.program.model.data.Structure;
+
+/**
+ * {@link CoordinatedStructureLine} for showing structure description (its comment).
+ */
+public class StructureDescriptionLine extends CoordinatedStructureLine {
+
+ /**
+ * Constructor
+ * @param model the {@link CoordinatedStructureModel}
+ * @param leftStruct the left structure
+ * @param rightStruct the right structure
+ * @param mergedStruct the merged structure
+ * @param line the line number where this component will be shown in the overall list of
+ * line items (including name, description, info, etc.)
+ */
+ public StructureDescriptionLine(CoordinatedStructureModel model, Structure leftStruct,
+ Structure rightStruct, Structure mergedStruct, int line) {
+ super(model);
+ this.left = new DescriptionItem(leftStruct, mergedStruct, line);
+ this.right = new DescriptionItem(rightStruct, mergedStruct, line);
+ this.merged = new DescriptionItem(mergedStruct, mergedStruct, line);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("Left name: ");
+ buf.append(((DescriptionItem) left).struct.getName());
+ buf.append(", Right name: ");
+ buf.append(((DescriptionItem) right).struct.getName());
+ buf.append(", Merged name: ");
+ buf.append(((DescriptionItem) merged).struct.getName());
+
+ return buf.toString();
+ }
+
+ /**
+ * Class for the individual {@link ComparisonItem}s for each of the structures.
+ */
+ private class DescriptionItem extends ComparisonItem {
+ private static final int STRUCT_COMMENT_COL = 0;
+ private Structure struct;
+ private Structure mergedStruct;
+
+ DescriptionItem(Structure struct, Structure mergedStruct, int line) {
+ super("Structure Comment", line);
+ this.struct = struct;
+ this.mergedStruct = mergedStruct;
+ }
+
+ @Override
+ public String getColumnText(int column) {
+ if (column != STRUCT_COMMENT_COL) {
+ return "";
+ }
+ String description = struct.getDescription();
+
+ if (!StringUtils.isBlank(description)) {
+ description = "// " + description;
+ }
+ return description;
+ }
+
+ @Override
+ public boolean canApplyAny() {
+ return !isApplied(STRUCT_COMMENT_COL);
+ }
+
+ @Override
+ public boolean isAppliable() {
+ return true;
+ }
+
+ @Override
+ public boolean isAppliable(int column) {
+ if (struct == mergedStruct) {
+ return false; // cant apply the results to itself
+ }
+ return column == STRUCT_COMMENT_COL;
+ }
+
+ @Override
+ public boolean isApplied(int column) {
+ if (column == STRUCT_COMMENT_COL) {
+ return Objects.equals(struct.getDescription(), mergedStruct.getDescription());
+ }
+ return false;
+ }
+
+ @Override
+ public int getMinWidth(int column) {
+ return column == STRUCT_COMMENT_COL ? 200 : 0;
+ }
+
+ @Override
+ public int hashCode() {
+ return struct.getName().hashCode();
+ }
+
+ @Override
+ public String toString() {
+ String description = struct.getDescription();
+ if (StringUtils.isBlank(description)) {
+ return "";
+ }
+ return "// " + description;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ DescriptionItem other = (DescriptionItem) obj;
+ return Objects.equals(struct.getDescription(), other.struct.getDescription());
+ }
+
+ @Override
+ public void applyAll() {
+ String description = struct.getDescription();
+ mergedStruct.setDescription(description);
+ modelChanged();
+ }
+
+ @Override
+ public boolean isBlank() {
+ return StringUtils.isBlank(struct.getDescription());
+ }
+ }
+}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/StructureInfoLine.java b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/StructureInfoLine.java
new file mode 100644
index 0000000000..b096972303
--- /dev/null
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/StructureInfoLine.java
@@ -0,0 +1,116 @@
+/* ###
+ * 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.merge.structures;
+
+import java.util.Objects;
+
+/**
+ * {@link CoordinatedStructureLine} for showing invariant structure information. This includes
+ * syntax ("{" and "}") and structure details (size, alignment, packing).
+ */
+public class StructureInfoLine extends CoordinatedStructureLine {
+
+ /**
+ * Constructor
+ * @param model the {@link CoordinatedStructureModel}
+ * @param left the string to be displayed in the left display
+ * @param right the string to be displayed in the right display
+ * @param merged the string to be displayed in the merged display
+ * @param line the line number of this line in the overall display
+ * @param type the type of info ("Syntax", or "Structure details")
+ */
+ public StructureInfoLine(CoordinatedStructureModel model, String left, String right,
+ String merged, int line, String type) {
+ super(model);
+ this.left = new InfoItem(left, type, line);
+ this.right = new InfoItem(right, type, line);
+ this.merged = new InfoItem(merged, type, line);
+ }
+
+ /**
+ * Constructor
+ * @param model the {@link CoordinatedStructureModel}
+ * @param all the string to be displayed in all displays
+ * @param line the line number of this line in the overall display
+ * @param type the type of info ("Syntax", or "Structure details")
+ */
+ public StructureInfoLine(CoordinatedStructureModel model, String all, int line, String type) {
+ this(model, all, all, all, line, type);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("left: ");
+ buf.append(((InfoItem) left).info);
+ buf.append(", right: ");
+ buf.append(((InfoItem) right).info);
+ buf.append(", merged: ");
+ buf.append(((InfoItem) merged).info);
+ return buf.toString();
+ }
+
+ /**
+ * Class for the individual {@link ComparisonItem}s for each of the structures.
+ */
+ private class InfoItem extends ComparisonItem {
+
+ private String info;
+
+ InfoItem(String info, String type, int line) {
+ super(type, line);
+ this.info = info;
+ }
+
+ @Override
+ public String getColumnText(int column) {
+ if (column == 0) {
+ return info;
+ }
+ return "";
+ }
+
+ @Override
+ public int getMinWidth(int column) {
+ return column == 0 ? 350 : 0;
+ }
+
+ @Override
+ public int hashCode() {
+ return info.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ InfoItem other = (InfoItem) obj;
+ return Objects.equals(info, other.info);
+ }
+
+ @Override
+ public String toString() {
+ return info;
+ }
+ }
+}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/StructureMergeDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/StructureMergeDialog.java
new file mode 100644
index 0000000000..77b7c69f6c
--- /dev/null
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/StructureMergeDialog.java
@@ -0,0 +1,420 @@
+/* ###
+ * 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.merge.structures;
+
+import java.awt.*;
+import java.awt.event.MouseEvent;
+
+import javax.swing.*;
+import javax.swing.event.ChangeEvent;
+
+import docking.*;
+import docking.action.DockingAction;
+import docking.action.builder.ActionBuilder;
+import docking.widgets.button.GRadioButton;
+import ghidra.app.merge.structures.CoordinatedStructureLine.CompareId;
+import ghidra.program.model.data.Structure;
+import ghidra.util.HelpLocation;
+import ghidra.util.MessageType;
+import utility.function.ExceptionalConsumer;
+
+/**
+ * Dialog for merging structures. The dialog is constructed given two structures and it will
+ * merge them, producing a third merged structure. The dialog will then display all three
+ * structures and provide controls for dealing with conflicts, allowing the user to choose
+ * components from the left or right side structures.
+ *
+ * The dialog itself doesn't do anything with the resulting merged structure. Clients need
+ * to provide an apply consumer that will be called when the user presses the dialog's apply
+ * button.
+ *
+ * The dialog also provides the following actions as keyboard only actions:
+ *
+ * - Apply Item (<SPACE>): pressing the space bar key will apply the currently focussed and
+ * selected item from either the left side or right sided. (Assuming it is appliable).
+ * - Focus Left Side (<LEFT ARROW>): pressing the left arrow will give focus to the left side
+ * display.
+ * - Focus Right Side (<RIGHT ARROW>): pressing the right arrow will give focus to the right side
+ * display.
+ *
+ */
+public class StructureMergeDialog extends DialogComponentProvider {
+ private CoordinatedStructureModel model;
+ private CoordinatedStructureDisplay mergedDisplay;
+ private CoordinatedStructureDisplay leftDisplay;
+ private CoordinatedStructureDisplay rightDisplay;
+ private LeftRightButtonPanel leftRightChooserPanel;
+ private DisplayCoordinator coordinator;
+ private ExceptionalConsumer applyConsumer;
+
+ /**
+ * Constructor
+ * @param title the dialog title.
+ * @param struct1 the first structure (will receive precedence for any conflicting components
+ * @param struct2 the second structure
+ * @param applyConsumer the consumer to call when the user presses the apply button. This
+ * consumer can throw an exception which will be displayed in the dialog and the dialog won't
+ * close. If the apply does not throw an exception, the dialog will be closed.
+ */
+ public StructureMergeDialog(String title, Structure struct1, Structure struct2,
+ ExceptionalConsumer applyConsumer) {
+ super(title);
+ this.applyConsumer = applyConsumer;
+ setHelpLocation(new HelpLocation("DataTypeManagerPlugin", "MergeStructures"));
+
+ model = new CoordinatedStructureModel(struct1, struct2, msg -> handleError(msg));
+
+ buildDisplays();
+ addWorkPanel(buildMainPanel());
+ addActions();
+
+ addApplyButton();
+ addCancelButton();
+ rootPanel.setFocusCycleRoot(true);
+ rootPanel.setFocusTraversalPolicy(new StructureMergeDialogFocusTraveralPolicy());
+ }
+
+ private void addActions() {
+ DockingAction applyAction = new ActionBuilder("Apply", getClass().getSimpleName())
+ .keyBinding("SPACE")
+ .description("Applies the selected structure component to the merged structure.")
+ .withContext(StructureMergeDialogContext.class)
+ .onAction(c -> toggleApply(c.getComparisonItem()))
+ .build();
+ addAction(applyAction);
+
+ DockingAction goToLeftAction =
+ new ActionBuilder("Left Display Action", getClass().getSimpleName())
+ .keyBinding("LEFT")
+ .description(
+ "Give keyboard focus to left side view in structure merger dialog.")
+ .onAction(c -> leftDisplay.getList().requestFocus())
+ .build();
+ addAction(goToLeftAction);
+ DockingAction goToRightAction =
+ new ActionBuilder("Right Display Action", getClass().getSimpleName())
+ .keyBinding("RIGHT")
+ .description(
+ "Give keyboard focus to right side view in structure merger dialog.")
+ .onAction(c -> rightDisplay.getList().requestFocus())
+ .build();
+ addAction(goToRightAction);
+
+ }
+
+ private void toggleApply(ComparisonItem item) {
+ if (item == null) {
+ return;
+ }
+ if (item.canApplyAny()) {
+ item.applyAll();
+ }
+ else if (item.canClear()) {
+ item.clear();
+ }
+ }
+
+ @Override
+ public ActionContext getActionContext(MouseEvent event) {
+ Component c = getComponent();
+ KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
+ Component focusedComponent = kfm.getFocusOwner();
+ if (focusedComponent != null && SwingUtilities.isDescendingFrom(focusedComponent, c)) {
+ c = focusedComponent;
+ }
+
+ if (event != null) {
+ Component sourceComponent = event.getComponent();
+ if (sourceComponent != null) {
+ c = sourceComponent;
+ }
+ }
+
+ CoordinatedStructureDisplay display = findDisplayForComponent(c);
+ return new StructureMergeDialogContext(this, c, display);
+ }
+
+ private CoordinatedStructureDisplay findDisplayForComponent(Component c) {
+
+ if (SwingUtilities.isDescendingFrom(c, leftDisplay)) {
+ return leftDisplay;
+ }
+ if (SwingUtilities.isDescendingFrom(c, rightDisplay)) {
+ return rightDisplay;
+ }
+ if (SwingUtilities.isDescendingFrom(c, leftDisplay)) {
+ return mergedDisplay;
+ }
+ return null;
+ }
+
+ @Override
+ protected void applyCallback() {
+ try {
+ applyConsumer.accept(model.getMergedStructure());
+ close();
+ }
+ catch (Exception e) {
+ setStatusText("Apply Failed: " + e.getMessage(), MessageType.ERROR);
+ }
+ }
+
+ private void handleError(String errorMsg) {
+ setStatusText(errorMsg);
+ }
+
+ private void buildDisplays() {
+ coordinator = new DisplayCoordinator();
+ leftDisplay = new CoordinatedStructureDisplay("Struct 1",
+ new StructDisplayModel(model, CompareId.LEFT), coordinator);
+ rightDisplay = new CoordinatedStructureDisplay("Struct 2",
+ new StructDisplayModel(model, CompareId.RIGHT), coordinator);
+ mergedDisplay = new CoordinatedStructureDisplay("Merged",
+ new StructDisplayModel(model, CompareId.MERGED), coordinator);
+ leftRightChooserPanel = new LeftRightButtonPanel();
+ int rowHeight = Math.max(leftDisplay.getRowHeight(), leftRightChooserPanel.getRowHeight());
+ leftRightChooserPanel.setRowHeight(rowHeight);
+ leftDisplay.setRowHeight(rowHeight);
+ rightDisplay.setRowHeight(rowHeight);
+ mergedDisplay.setRowHeight(rowHeight);
+ }
+
+ private JComponent buildMainPanel() {
+ JPanel panel = new JPanel(new GridLayout(2, 1, 10, 10));
+ panel.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
+ panel.add(buildSourcePanel());
+ panel.add(mergedDisplay);
+ return panel;
+ }
+
+ /**
+ * Customized focus traversal policy to avoid traversing to any of the apply buttons. The
+ * focus will go as follows: left display, right display, merged display, apply button, and
+ * finally cancel button.
+ */
+ private class StructureMergeDialogFocusTraveralPolicy extends FocusTraversalPolicy {
+
+ @Override
+ public Component getComponentAfter(Container aContainer, Component aComponent) {
+ if (SwingUtilities.isDescendingFrom(aComponent, leftDisplay)) {
+ return rightDisplay.getList();
+ }
+ if (SwingUtilities.isDescendingFrom(aComponent, rightDisplay)) {
+ return mergedDisplay.getList();
+ }
+ if (SwingUtilities.isDescendingFrom(aComponent, mergedDisplay)) {
+ return applyButton;
+ }
+ if (aComponent == applyButton) {
+ return cancelButton;
+ }
+ return leftDisplay.getList();
+
+ }
+
+ @Override
+ public Component getComponentBefore(Container aContainer, Component aComponent) {
+ if (aComponent == cancelButton) {
+ return applyButton;
+ }
+ if (aComponent == applyButton) {
+ return mergedDisplay.getList();
+ }
+ if (SwingUtilities.isDescendingFrom(aComponent, mergedDisplay)) {
+ return rightDisplay.getList();
+ }
+ if (SwingUtilities.isDescendingFrom(aComponent, rightDisplay)) {
+ return leftDisplay.getList();
+ }
+ return cancelButton;
+ }
+
+ @Override
+ public Component getFirstComponent(Container aContainer) {
+ return leftDisplay.getList();
+ }
+
+ @Override
+ public Component getLastComponent(Container aContainer) {
+ return cancelButton;
+ }
+
+ @Override
+ public Component getDefaultComponent(Container aContainer) {
+ return leftDisplay.getList();
+ }
+
+ }
+
+ private Component buildSourcePanel() {
+ // Using grid bag layout so that the two structure displays on either side of the
+ // button panel get all the available extra space. The middle button panel is always
+ // fixed width.
+
+ JPanel panel = new JPanel(new GridBagLayout());
+ GridBagConstraints gbc = new GridBagConstraints();
+ gbc.gridx = 0;
+ gbc.gridy = 0;
+ gbc.gridwidth = 1;
+ gbc.gridheight = 1;
+ gbc.fill = GridBagConstraints.BOTH;
+ gbc.weightx = 0.5;
+ gbc.weighty = 1;
+ panel.add(leftDisplay, gbc);
+
+ gbc.gridx = 2;
+ panel.add(rightDisplay, gbc);
+
+ gbc.gridx = 1;
+ gbc.weightx = 0.0;
+ panel.add(leftRightChooserPanel, gbc);
+
+ return panel;
+ }
+
+ private class LeftRightButtonPanel extends JPanel {
+ private static int BUTTON_GAP = 7;
+ private int rowHeight;
+ private int buttonWidth;
+ private int buttonHeight;
+ private JPanel radioButtonPanel;
+ private JViewport buttonPanelViewport;
+
+ LeftRightButtonPanel() {
+ super(new BorderLayout());
+ Insets insets = leftDisplay.getInsets();
+ setBorder(BorderFactory.createEmptyBorder(insets.top, 1, insets.bottom, 1));
+ GRadioButton button = new GRadioButton();
+ Dimension preferredButtonSize = button.getPreferredSize();
+ rowHeight = preferredButtonSize.height;
+ buttonHeight = preferredButtonSize.height;
+ buttonWidth = preferredButtonSize.width;
+ radioButtonPanel = new JPanel(null);
+
+ // we need to set the preferred size of our inner panel to be at least as big as the
+ // the corresponding coordinatedStructureViews or the scrolling doesn't work correctly.
+ // if it is smaller, it limits the scroll position to its preferred size.
+ radioButtonPanel.setPreferredSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE));
+ buttonPanelViewport = new JViewport();
+ buttonPanelViewport.setView(radioButtonPanel);
+ add(buttonPanelViewport, BorderLayout.CENTER);
+ leftDisplay.addViewportListener(e -> viewportChanged(e));
+ model.addChangeListener(() -> buildButtons());
+ }
+
+ private void viewportChanged(ChangeEvent e) {
+ JViewport viewport = (JViewport) e.getSource();
+ buttonPanelViewport.setViewSize(viewport.getViewSize());
+ buttonPanelViewport.setViewPosition(new Point(0, viewport.getViewPosition().y));
+ buildButtons();
+ }
+
+ int getRowHeight() {
+ return rowHeight;
+ }
+
+ void setRowHeight(int rowHeight) {
+ this.rowHeight = rowHeight;
+ }
+
+ @Override
+ public Dimension getPreferredSize() {
+ Insets insets = getInsets();
+ return new Dimension(2 * buttonWidth + BUTTON_GAP + insets.left + insets.right, 0);
+ }
+
+ @Override
+ public Dimension getMinimumSize() {
+ return getPreferredSize();
+ }
+
+ private void buildButtons() {
+ int index1 = leftDisplay.getFirstVisibleIndex();
+ int index2 = leftDisplay.getLastVisibleIndex();
+ buildButtons(index1, index2);
+ repaint();
+ }
+
+ public void buildButtons(int firstIndex, int lastIndex) {
+ if (firstIndex < 0) {
+ return;
+ }
+ radioButtonPanel.removeAll();
+
+ for (int i = firstIndex; i <= lastIndex; i++) {
+ int y = i * rowHeight + CoordinatedStructureDisplay.MARGIN;
+ ComparisonItem leftItem = leftDisplay.getItem(i);
+ ComparisonItem rightItem = rightDisplay.getItem(i);
+ if (leftItem.isAppliable()) {
+ buildButton(0, y, leftItem, true);
+ }
+ if (rightItem.isAppliable()) {
+ buildButton(buttonWidth + BUTTON_GAP, y, rightItem, false);
+ }
+ }
+ }
+
+ private void buildButton(int x, int y, ComparisonItem item, boolean isLeft) {
+ GRadioButton button = new GRadioButton();
+ button.setBounds(x, y, buttonWidth, buttonHeight);
+ button.setSelected(!item.canApplyAny());
+ button.addActionListener(e -> {
+ if (!button.isSelected() && !item.canClear()) {
+ button.setSelected(true);
+ return;
+ }
+ coordinator.setChanging(true);
+ try {
+ if (button.isSelected()) {
+ item.applyAll();
+ }
+ else {
+ item.clear();
+ }
+
+ // We need to validate the mergedDisplay with the coordinator disabled.
+ // Otherwise, it will revalidate on the next repaint which may cause it to
+ // resize, which in turn will move its viewport, which then affects the source
+ // panel, causing them to jump as buttons are pressed.
+ mergedDisplay.validate();
+ CoordinatedStructureDisplay focusDisplay = isLeft ? leftDisplay : rightDisplay;
+ focusDisplay.getList().requestFocus();
+ }
+ finally {
+ coordinator.setChanging(false);
+ }
+ });
+ radioButtonPanel.add(button);
+ }
+
+ }
+
+ private class StructureMergeDialogContext extends DefaultActionContext {
+
+ private CoordinatedStructureDisplay display;
+
+ public StructureMergeDialogContext(DialogComponentProvider dialog, Component source,
+ CoordinatedStructureDisplay display) {
+ super(null, dialog, source);
+ this.display = display;
+ }
+
+ public ComparisonItem getComparisonItem() {
+ return display != null ? display.getSelectedItem() : null;
+ }
+ }
+
+}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/StructureNameLine.java b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/StructureNameLine.java
new file mode 100644
index 0000000000..6d256a823d
--- /dev/null
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/merge/structures/StructureNameLine.java
@@ -0,0 +1,165 @@
+/* ###
+ * 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.merge.structures;
+
+import java.util.Objects;
+
+import ghidra.program.model.data.Structure;
+import ghidra.util.InvalidNameException;
+import ghidra.util.exception.DuplicateNameException;
+
+/**
+ * {@link CoordinatedStructureLine} for showing the structure's name.
+ */
+public class StructureNameLine extends CoordinatedStructureLine {
+
+ /**
+ * Constructor
+ * @param model the {@link CoordinatedStructureModel}
+ * @param leftStruct the left structure
+ * @param rightStruct the right structure
+ * @param mergedStruct the merged structure
+ * @param line the line number where this component will be shown in the overall list of
+ * line items (including name, description, info, etc.)
+ */
+ public StructureNameLine(CoordinatedStructureModel model, Structure leftStruct,
+ Structure rightStruct, Structure mergedStruct, int line) {
+ super(model);
+ this.left = new NameItem(leftStruct, mergedStruct, line);
+ this.right = new NameItem(rightStruct, mergedStruct, line);
+ this.merged = new NameItem(mergedStruct, mergedStruct, line);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("Left name: ");
+ buf.append(((NameItem) left).struct.getName());
+ buf.append(", Right name: ");
+ buf.append(((NameItem) right).struct.getName());
+ buf.append(", Merged name: ");
+ buf.append(((NameItem) merged).struct.getName());
+
+ return buf.toString();
+ }
+
+ /**
+ * Class for the individual {@link ComparisonItem}s for each of the structures.
+ */
+ private class NameItem extends ComparisonItem {
+ private static final int STRUCT_KEYWORD_COL = 0;
+ private static final int STRUCT_NAME_COL = 1;
+ private Structure struct;
+ private Structure mergedStruct;
+
+ NameItem(Structure struct, Structure mergedStruct, int line) {
+ super("Structure Name", line);
+ this.struct = struct;
+ this.mergedStruct = mergedStruct;
+ }
+
+ @Override
+ public String getColumnText(int column) {
+ switch (column) {
+ case STRUCT_KEYWORD_COL:
+ return "Struct";
+ case STRUCT_NAME_COL:
+ return struct.getName();
+ default:
+ return "";
+ }
+ }
+
+ @Override
+ public boolean canApplyAny() {
+ return !isApplied(STRUCT_NAME_COL);
+ }
+
+ @Override
+ public boolean isAppliable() {
+ return true;
+ }
+
+ @Override
+ public boolean isAppliable(int column) {
+ if (struct == mergedStruct) {
+ return false; // cant apply the results to itself
+ }
+ return column == STRUCT_NAME_COL;
+ }
+
+ @Override
+ public boolean isApplied(int column) {
+ if (column == STRUCT_NAME_COL) {
+ return struct.getName().equals(mergedStruct.getName());
+ }
+ return false;
+ }
+
+ @Override
+ public int getMinWidth(int column) {
+ switch (column) {
+ case 0:
+ return -1;
+ case 1:
+ return 200;
+ default:
+ return 0;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return struct.getName().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ NameItem other = (NameItem) obj;
+ return Objects.equals(struct.getName(), other.struct.getName());
+ }
+
+ @Override
+ public String toString() {
+ return "Struct " + struct.getName();
+ }
+
+ @Override
+ public void applyAll() {
+ try {
+ mergedStruct.setName(struct.getName());
+ modelChanged();
+ }
+ catch (InvalidNameException | DuplicateNameException e) {
+ error("Error applying structure name: " + e.getMessage());
+ }
+ }
+
+ @Override
+ public boolean isBlank() {
+ return false;
+ }
+ }
+}
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
index 016a5f22b9..cadaf854a4 100644
--- 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
@@ -24,6 +24,7 @@ import docking.action.DockingAction;
import docking.action.MenuData;
import docking.widgets.label.GLabel;
import docking.widgets.tree.GTree;
+import ghidra.app.merge.structures.StructureMergeDialog;
import ghidra.app.plugin.core.datamgr.DataTypeManagerPlugin;
import ghidra.app.plugin.core.datamgr.DataTypesActionContext;
import ghidra.app.plugin.core.datamgr.tree.DataTypeNode;
@@ -36,9 +37,9 @@ 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.*;
import ghidra.util.data.DataTypeParser.AllowedDataTypes;
+import ghidra.util.exception.DuplicateNameException;
import ghidra.util.layout.VerticalLayout;
/**
@@ -72,7 +73,7 @@ public class MergeDataTypeAction extends DockingAction {
if (!DataTypeUtilities.supportsMerge(dataType)) {
return false;
}
-
+
DataTypeManager dataTypeManager = dataType.getDataTypeManager();
// for now, only allow merging on program datatypes.
@@ -143,16 +144,25 @@ public class MergeDataTypeAction extends DockingAction {
}
private void merge(DataType mergeToDt, DataType mergeFromDt) {
+ if (mergeToDt == mergeFromDt) {
+ Msg.showError(this, null, "Merge Failed", "You can't merge a datatype with itself!");
+ return;
+ }
+
try {
+ // we have a specialized interactive structure merger available
+ if ((mergeToDt instanceof Structure mergeToStruct) &&
+ (mergeFromDt instanceof Structure mergeFromStruct)) {
+ mergeStructures(mergeToStruct, mergeFromStruct);
+ return;
+ }
+
+ // otherwise, fall back to a generic non-interactive merger
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);
+ performMerge(mergeToDt, mergeFromDt, merged);
}
}
catch (DataTypeMergeException e) {
@@ -160,7 +170,7 @@ public class MergeDataTypeAction extends DockingAction {
new DataTypeMergeErrorDialog(mergeToDt, mergeFromDt, e.getMessage());
DockingWindowManager.showDialog(dialog);
}
- catch (DataTypeDependencyException e) {
+ catch (Exception e) {
Msg.showError(this, null, "Merge Failed",
"Merge failed. Existing type '%s', replacement type '%s'.".formatted(
mergeFromDt.getName(),
@@ -169,6 +179,26 @@ public class MergeDataTypeAction extends DockingAction {
}
}
+ private void performMerge(DataType mergeToDt, DataType mergeFromDt, DataType merged)
+ throws IllegalArgumentException, DataTypeDependencyException, InvalidNameException,
+ DuplicateNameException {
+ 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);
+ if (!mergeToDt.getName().equals(merged.getName())) {
+ mergeToDt.setName(merged.getName());
+ }
+ }
+
+ private void mergeStructures(Structure mergeToStruct, Structure mergeFromStruct) {
+ StructureMergeDialog dialog =
+ new StructureMergeDialog("Merge Structures", mergeToStruct, mergeFromStruct,
+ s -> performMerge(mergeToStruct, mergeFromStruct, s));
+ DockingWindowManager.showDialog(dialog);
+ }
+
private boolean confirmMerger(DataTypeMerger> merger, DataType merged, DataType mergeTo,
DataType mergeFrom) {
DataTypeMergeConfirmationDialog dialog =
@@ -199,7 +229,7 @@ public class MergeDataTypeAction extends DockingAction {
updatedPanel.setBorder(BorderFactory.createEmptyBorder(5, 10, 10, 0));
updatedPanel.setLayout(new VerticalLayout(5));
- GLabel label = new GLabel("Choose the data type to merge: ");
+ GLabel label = new GLabel("Choose the data type to merge from: ");
label.setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0));
updatedPanel.add(label);
diff --git a/Ghidra/Features/Base/src/test/java/ghidra/app/merge/structures/CoordinatedStructureModelTest.java b/Ghidra/Features/Base/src/test/java/ghidra/app/merge/structures/CoordinatedStructureModelTest.java
new file mode 100644
index 0000000000..48e571e851
--- /dev/null
+++ b/Ghidra/Features/Base/src/test/java/ghidra/app/merge/structures/CoordinatedStructureModelTest.java
@@ -0,0 +1,642 @@
+/* ###
+ * 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.merge.structures;
+
+import static ghidra.app.merge.structures.CoordinatedStructureModelTest.ApplyState.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.util.function.Consumer;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import docking.DockingWindowManager;
+import ghidra.program.database.data.merge.StructureBuilder;
+import ghidra.program.model.data.*;
+import ghidra.test.AbstractGhidraHeadedIntegrationTest;
+import ghidra.test.TestEnv;
+import ghidra.util.UniversalIdGenerator;
+import utility.function.Dummy;
+
+public class CoordinatedStructureModelTest extends AbstractGhidraHeadedIntegrationTest {
+ enum ApplyState {
+ YES, NO, NA
+ }
+
+ private DataType wordDt;
+ private DataType dwordDt;
+ private IntegerDataType intDt;
+ private DataType zeroArrayDt;
+ private Consumer errorHandler;
+ private CoordinatedStructureModel model;
+ private Structure struct1;
+ private Structure struct2;
+
+ @Before
+ public void setUp() throws Exception {
+ UniversalIdGenerator.initialize();
+ wordDt = new WordDataType();
+ dwordDt = new DWordDataType();
+ intDt = new IntegerDataType();
+ zeroArrayDt = new ArrayDataType(intDt, 0);
+ this.errorHandler = e -> reportError(e);
+ }
+
+ @Test
+ public void testStructName() {
+ struct1 = new StructureBuilder("A", 8)
+ .build();
+
+ struct2 = new StructureBuilder("B", 8)
+ .build();
+
+ createModel();
+
+ assertLeft(0, "Struct A");
+ assertRight(0, "Struct B");
+ assertResult(0, "Struct A");
+
+ assertAppliable(0, NO, YES);
+
+ applyRight(0);
+ assertResult(0, "Struct B");
+ assertAppliable(0, YES, NO);
+
+ applyLeft(0);
+ assertResult(0, "Struct A");
+ assertAppliable(0, NO, YES);
+ }
+
+ private void createModel() {
+ model = new CoordinatedStructureModel(struct1, struct2, errorHandler);
+
+ // un-comment the following line out to show the dialog for for debugging purposes
+ // showDialog();
+ }
+
+ @Test
+ public void testStructDescription() {
+ struct1 = new StructureBuilder("A", 8)
+ .description("Comment A")
+ .build();
+
+ struct2 = new StructureBuilder("B", 8)
+ .description("Comment B")
+ .build();
+ createModel();
+
+ assertLeft(0, "// Comment A");
+ assertRight(0, "// Comment B");
+ assertResult(0, "// Comment A");
+ assertAppliable(0, NO, YES);
+
+ applyRight(0);
+ assertResult(0, "// Comment B");
+ assertAppliable(0, YES, NO);
+
+ applyLeft(0);
+ assertResult(0, "// Comment A");
+ assertAppliable(0, NO, YES);
+ }
+
+ @Test
+ public void testStructDescriptionOneSideBlank() {
+ struct1 = new StructureBuilder("A", 8)
+ .build();
+
+ struct2 = new StructureBuilder("B", 8)
+ .description("Comment B")
+ .build();
+ createModel();
+
+ assertLeft(0, "");
+ assertRight(0, "// Comment B");
+ assertResult(0, "// Comment B");
+ assertAppliable(0, YES, NO);
+
+ applyLeft(0);
+ assertResult(0, "");
+ assertAppliable(0, NO, YES);
+
+ applyRight(0);
+ assertResult(0, "// Comment B");
+ assertAppliable(0, YES, NO);
+ }
+
+ @Test
+ public void testComponentSameOffsetDatatypesDiffer() {
+ struct1 = new StructureBuilder("A", 10)
+ .add(0, intDt, "aaa")
+ .build();
+
+ struct2 = new StructureBuilder("B", 10)
+ .add(0, wordDt, "xxx")
+ .build();
+ createModel();
+
+ assertLeft(3, "0 int aaa");
+ assertRight(3, "0 word xxx");
+ assertResult(3, "0 int aaa");
+
+ assertAppliable(3, NO, YES);
+
+ applyRight(3);
+ assertLeft(3, "0 int aaa");
+ assertRight(3, "0 word xxx");
+ assertResult(3, "0 word xxx");
+
+ assertAppliable(3, YES, NO);
+
+ applyLeft(3);
+ assertLeft(3, "0 int aaa");
+ assertRight(3, "0 word xxx");
+ assertResult(3, "0 int aaa");
+ }
+
+ @Test
+ public void testComponentSameOffsetOnlyNameDiffers() {
+ struct1 = new StructureBuilder("A", 8)
+ .add(0, intDt, "foo")
+ .build();
+
+ struct2 = new StructureBuilder("B", 8)
+ .add(0, intDt, "bar")
+ .build();
+ createModel();
+
+ // line 0 is struct name
+ // line 1 is struct size, alignment and packing
+ // line 2 is {
+ // line 3 is component line
+
+ assertLeft(3, "0 int foo");
+ assertRight(3, "0 int bar");
+ assertResult(3, "0 int foo");
+
+ assertAppliable(3, NO, YES);
+
+ applyRight(3);
+ assertResult(3, "0 int bar");
+ assertAppliable(3, YES, NO);
+
+ applyLeft(3);
+ assertResult(3, "0 int foo");
+ assertAppliable(3, NO, YES);
+ }
+
+ @Test
+ public void testOtherTypeApplieNoName() {
+ struct1 = new StructureBuilder("A", 8)
+ .build();
+
+ struct2 = new StructureBuilder("B", 8)
+ .add(0, intDt, null)
+ .build();
+ createModel();
+
+ assertLeft(3, "0 undefined (4)");
+ assertRight(3, "0 int");
+ assertResult(3, "0 int");
+
+ assertAppliable(3, NA, NO);
+
+ clearRight(3);
+ assertResult(3, "0 undefined (4)");
+ assertAppliable(3, NA, YES);
+
+ applyRight(3);
+ assertResult(3, "0 int");
+ assertAppliable(3, NA, NO);
+ }
+
+ @Test
+ public void testComponentSameOffsetOnlyCommentDiffers() {
+ struct1 = new StructureBuilder("A", 8)
+ .add(0, intDt, "foo", "aaa")
+ .build();
+
+ struct2 = new StructureBuilder("B", 8)
+ .add(0, intDt, "foo", "bbb")
+ .build();
+ createModel();
+
+ // line 0 is struct name
+ // line 1 is struct size, alignment and packing
+ // line 2 is {
+ // line 3 is component line
+
+ assertLeft(3, "0 int foo // aaa");
+ assertRight(3, "0 int foo // bbb");
+ assertResult(3, "0 int foo // aaa");
+
+ assertAppliable(3, NO, YES);
+
+ applyRight(3);
+ assertResult(3, "0 int foo // bbb");
+ assertAppliable(3, YES, NO);
+
+ applyLeft(3);
+ assertResult(3, "0 int foo // aaa");
+ assertAppliable(3, NO, YES);
+ }
+
+ @Test
+ public void testComponentSameOffsetDifferentNameRightAddsComment() {
+ struct1 = new StructureBuilder("A", 8)
+ .add(0, intDt, "foo")
+ .build();
+
+ struct2 = new StructureBuilder("B", 8)
+ .add(0, intDt, "bar", "bbb")
+ .build();
+ createModel();
+
+ // line 0 is struct name
+ // line 1 is struct size, alignment and packing
+ // line 2 is {
+ // line 3 is component line
+
+ assertLeft(3, "0 int foo");
+ assertRight(3, "0 int bar // bbb");
+ assertResult(3, "0 int foo // bbb");
+
+ assertAppliable(3, YES, YES);
+
+ applyRight(3);
+ assertResult(3, "0 int bar // bbb");
+ assertAppliable(3, YES, NO);
+
+ applyLeft(3);
+ assertResult(3, "0 int foo");
+ assertAppliable(3, NO, YES);
+ }
+
+ @Test
+ public void testComponentOffcut() {
+ struct1 = new StructureBuilder("A", 10)
+ .add(0, intDt, "aaa")
+ .add(4, intDt, "bbb")
+ .build();
+
+ struct2 = new StructureBuilder("B", 10)
+ .add(1, intDt, "xxx")
+ .add(5, intDt, "yyy")
+ .build();
+ createModel();
+
+ assertLeft(3, "0 int aaa");
+ assertLeft(4, "");
+ assertLeft(5, "4 int bbb");
+ assertLeft(6, "");
+
+ assertRight(3, "0 undefined (1)");
+ assertRight(4, "1 int xxx");
+ assertRight(5, "");
+ assertRight(6, "5 int yyy");
+
+ assertResult(3, "0 int aaa");
+ assertResult(4, "");
+ assertResult(5, "4 int bbb");
+ assertResult(6, "");
+
+ assertAppliable(3, NO, NA);
+ assertAppliable(4, NA, YES);
+ assertAppliable(5, NO, NA);
+ assertAppliable(6, NA, YES);
+
+ // apply right side on line 4, line 3 and 5 that are defined on the left side are cleared
+ applyRight(4);
+
+ assertResult(3, "0 undefined (1)");
+ assertResult(4, "1 int xxx");
+ assertResult(5, "");
+ assertResult(6, "5 undefined (4)");
+
+ assertAppliable(3, YES, NA);
+ assertAppliable(4, NA, NO);
+ assertAppliable(5, YES, NA);
+ assertAppliable(6, NA, YES);
+
+ }
+
+ @Test
+ public void testZeroSizedArrays() {
+ struct1 = new StructureBuilder("A", 8)
+ .add(0, dwordDt, "foo", "comment")
+ .build();
+
+ struct2 = new StructureBuilder("B", 8)
+ .add(0, zeroArrayDt, "arrayX")
+ .add(0, zeroArrayDt, "arrayY")
+ .add(0, zeroArrayDt, "arrayZ")
+ .build();
+ createModel();
+
+ assertLeft(3, "");
+ assertLeft(4, "");
+ assertLeft(5, "");
+ assertLeft(6, "0 dword foo // comment");
+
+ assertRight(3, "0 int[0] arrayX");
+ assertRight(4, "0 int[0] arrayY");
+ assertRight(5, "0 int[0] arrayZ");
+ assertRight(6, "0 undefined (4)");
+
+ assertResult(3, "0 int[0] arrayX");
+ assertResult(4, "0 int[0] arrayY");
+ assertResult(5, "0 int[0] arrayZ");
+ assertResult(6, "0 dword foo // comment");
+
+ assertAppliable(3, NA, NO);
+ assertAppliable(4, NA, NO);
+ assertAppliable(5, NA, NO);
+ assertAppliable(6, NO, NA);
+
+ }
+
+ @Test
+ public void testBitFieldNoConflicts() throws Exception {
+ struct1 = new StructureBuilder("A", 10)
+ .bitField(0, 1, 0, 2, "A")
+ .bitField(0, 1, 5, 6, "B")
+ .build();
+
+ struct2 = new StructureBuilder("B", 10)
+ .bitField(0, 1, 3, 4, "C")
+ .bitField(0, 1, 7, 7, "D")
+ .build();
+ createModel();
+
+ assertLeft(3, "0 int:3 (0, 2) A");
+ assertLeft(4, "");
+ assertLeft(5, "0 int:2 (5, 6) B");
+ assertLeft(6, "");
+
+ assertRight(3, "");
+ assertRight(4, "0 int:2 (3, 4) C");
+ assertRight(5, "");
+ assertRight(6, "0 int:1 (7, 7) D");
+
+ assertResult(3, "0 int:3 (0, 2) A");
+ assertResult(4, "0 int:2 (3, 4) C");
+ assertResult(5, "0 int:2 (5, 6) B");
+ assertResult(6, "0 int:1 (7, 7) D");
+
+ assertAppliable(3, NO, NA);
+ assertAppliable(4, NA, NO);
+ assertAppliable(5, NO, NA);
+ assertAppliable(6, NA, NO);
+
+ }
+
+ @Test
+ public void testBitFieldConflicts() throws Exception {
+ struct1 = new StructureBuilder("A", 10)
+ .bitField(0, 1, 0, 2, "A")
+ .bitField(0, 1, 5, 6, "B")
+ .build();
+
+ struct2 = new StructureBuilder("B", 10)
+ .bitField(0, 1, 2, 7, "C")
+ .build();
+ createModel();
+
+ assertLeft(3, "0 int:3 (0, 2) A");
+ assertLeft(4, "");
+ assertLeft(5, "0 int:2 (5, 6) B");
+
+ assertRight(3, "");
+ assertRight(4, "0 int:6 (2, 7) C");
+ assertRight(5, "");
+
+ assertResult(3, "0 int:3 (0, 2) A");
+ assertResult(4, "");
+ assertResult(5, "0 int:2 (5, 6) B");
+
+ assertAppliable(3, NO, NA);
+ assertAppliable(4, NA, YES);
+ assertAppliable(5, NO, NA);
+
+ applyRight(4);
+
+ assertResult(3, "");
+ assertResult(4, "0 int:6 (2, 7) C");
+ assertResult(5, "");
+
+ assertAppliable(3, YES, NA);
+ assertAppliable(4, NA, NO);
+ assertAppliable(5, YES, NA);
+
+ }
+
+ @Test
+ public void testBitFieldConflictWithNonBitField() throws Exception {
+ struct1 = new StructureBuilder("A", 10)
+ .bitField(0, 2, 5, 12, "A")
+ .build();
+
+ struct2 = new StructureBuilder("B", 10)
+ .add(1, intDt, "B")
+ .build();
+ createModel();
+
+ assertLeft(3, "0 int:8 (5, 12) A");
+ assertLeft(4, "");
+
+ assertRight(3, "0 undefined (1)");
+ assertRight(4, "1 int B");
+
+ assertResult(3, "0 int:8 (5, 12) A");
+ assertResult(4, "");
+
+ assertAppliable(3, NO, NA);
+ assertAppliable(4, NA, YES);
+
+ applyRight(4);
+
+ assertResult(3, "0 undefined (1)");
+ assertResult(4, "1 int B");
+
+ assertAppliable(3, YES, NA);
+ assertAppliable(4, NA, NO);
+
+ }
+
+ @Test
+ public void testBitFieldInteractsWithZeroLengthArrays() throws Exception {
+ struct1 = new StructureBuilder("A", 10)
+ .bitField(0, 2, 5, 12, "A")
+ .build();
+
+ struct2 = new StructureBuilder("B", 10)
+ .add(0, zeroArrayDt, "B")
+ .add(0, zeroArrayDt, "C")
+ .build();
+ createModel();
+
+ assertLeft(3, "");
+ assertLeft(4, "");
+ assertLeft(5, "0 int:8 (5, 12) A");
+
+ assertRight(3, "0 int[0] B");
+ assertRight(4, "0 int[0] C");
+ assertRight(5, "0 undefined (2)");
+
+ assertResult(3, "0 int[0] B");
+ assertResult(4, "0 int[0] C");
+ assertResult(5, "0 int:8 (5, 12) A");
+
+ assertAppliable(3, NA, NO);
+ assertAppliable(4, NA, NO);
+ assertAppliable(5, NO, NA);
+ }
+
+ @Test
+ public void testBitFieldCommentDiffs() throws Exception {
+ struct1 = new StructureBuilder("A", 10)
+ .bitField(0, 2, 5, 12, "A", "Comment A")
+ .build();
+
+ struct2 = new StructureBuilder("B", 10)
+ .bitField(0, 2, 5, 12, "A", "Comment B")
+ .build();
+ createModel();
+
+ assertLeft(3, "0 int:8 (5, 12) A // Comment A");
+ assertRight(3, "0 int:8 (5, 12) A // Comment B");
+ assertResult(3, "0 int:8 (5, 12) A // Comment A");
+
+ assertAppliable(3, NO, YES);
+
+ applyRight(3);
+ assertResult(3, "0 int:8 (5, 12) A // Comment B");
+ }
+
+ @Test
+ public void testClear() throws Exception {
+ struct1 = new StructureBuilder("A", 10)
+ .add(0, intDt, "aaa")
+ .build();
+
+ struct2 = new StructureBuilder("B", 10)
+ .add(0, wordDt, "xxx")
+ .build();
+ createModel();
+
+ assertLeft(3, "0 int aaa");
+ assertRight(3, "0 word xxx");
+ assertResult(3, "0 int aaa");
+
+ assertAppliable(3, NO, YES);
+ clearLeft(3);
+ assertResult(3, "0 undefined (4)");
+ applyRight(3);
+ assertResult(3, "0 word xxx");
+ clearRight(3);
+ assertResult(3, "0 undefined (4)");
+
+ }
+
+ private void assertAppliable(int line, ApplyState left, ApplyState right) {
+ CoordinatedStructureLine compareLine = model.getLines().get(line);
+ assertAppliable("Left", line, compareLine.left, left);
+ assertAppliable("Right", line, compareLine.right, right);
+ }
+
+ private void assertAppliable(String side, int line, ComparisonItem item,
+ ApplyState expectedState) {
+ boolean appliable = item.isAppliable();
+
+ if (!appliable) {
+ if (expectedState != NA) {
+ fail("Expected %s side at line %d to be %s, but was NA".formatted(side, line,
+ expectedState));
+
+ }
+ }
+ else {
+ if (appliable && expectedState == NA) {
+ fail("Expected %s side at line %d to be NA, but was %s".formatted(side, line,
+ expectedState));
+ }
+ ApplyState state = item.canApplyAny() ? YES : NO;
+ assertEquals("Incorrect apply state on %s side at line %d".formatted(side, line),
+ expectedState, state);
+ }
+ }
+
+ private void applyRight(int line) {
+ ComparisonItem right = extracted(line);
+ right.applyAll();
+ }
+
+ private ComparisonItem extracted(int line) {
+ CoordinatedStructureLine compareLine = model.getLines().get(line);
+ ComparisonItem right = compareLine.right;
+ return right;
+ }
+
+ private void applyLeft(int line) {
+ CoordinatedStructureLine compareLine = model.getLines().get(line);
+ ComparisonItem left = compareLine.left;
+ left.applyAll();
+ }
+
+ private void clearLeft(int line) {
+ CoordinatedStructureLine compareLine = model.getLines().get(line);
+ ComparisonItem left = compareLine.left;
+ left.clear();
+ }
+
+ private void clearRight(int line) {
+ CoordinatedStructureLine compareLine = model.getLines().get(line);
+ ComparisonItem left = compareLine.left;
+ left.clear();
+ }
+
+ private void assertLeft(int line, String expected) {
+ CoordinatedStructureLine compareLine = model.getLines().get(line);
+ assertEquals("Left component at line " + line, expected, compareLine.left.toString());
+ }
+
+ private void assertRight(int line, String expected) {
+ CoordinatedStructureLine compareLine = model.getLines().get(line);
+ assertEquals("Right component at line " + line, expected, compareLine.right.toString());
+ }
+
+ private void assertResult(int line, String expected) {
+ CoordinatedStructureLine compareLine = model.getLines().get(line);
+ assertEquals("Result at line " + line + ": ", expected, compareLine.merged.toString());
+ }
+
+ private void reportError(String message) {
+ fail("Got unexpected exception: " + message);
+ }
+
+ private void showDialog() {
+ try {
+ TestEnv env = new TestEnv();
+ env.showTool();
+ StructureMergeDialog dialog = new StructureMergeDialog("Test Merge", struct1, struct2,
+ Dummy.exceptionalConsumer());
+ DockingWindowManager.showDialog(dialog);
+ }
+ catch (IOException e) {
+ failWithException("Faile to create TestEnv", e);
+ }
+ }
+}
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
index ab8c8007ba..649fec9490 100644
--- 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
@@ -175,9 +175,10 @@ public abstract class DataTypeMerger {
}
protected void mergeDescription() {
- String description1 = dt1.getDescription();
- String description2 = dt2.getDescription();
- String merged = join(description1, description2);
- working.setDescription(merged);
+ String description = dt1.getDescription();
+ if (StringUtils.isBlank(description)) {
+ description = dt2.getDescription();
+ }
+ working.setDescription(description);
}
}
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
index 4cb2af71a1..7bce68aa50 100644
--- 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
@@ -15,7 +15,9 @@
*/
package ghidra.program.database.data.merge;
-import java.util.List;
+import java.util.*;
+
+import org.apache.commons.lang3.StringUtils;
import ghidra.program.model.data.*;
@@ -24,19 +26,33 @@ import ghidra.program.model.data.*;
*/
public class StructureMerger extends DataTypeMerger {
+ private boolean terminateOnError;
+ private List errors = new ArrayList<>();
+
public StructureMerger(Structure struct1, Structure struct2) {
+ this(struct1, struct2, true);
+ }
+
+ public StructureMerger(Structure struct1, Structure struct2, boolean terminateOnError) {
super(struct1, struct2);
+ this.terminateOnError = terminateOnError;
}
@Override
public void doMerge() throws DataTypeMergeException {
- checkSizes();
mergeDescription();
- if (working.isPackingEnabled()) {
+ if (canMergePacked()) {
+ // very limited special case where all the datatypes line up and we are just merging
+ // names and comments.
mergePacked();
}
else {
+ // the result type needs to be unpacked for complex merges
+ if (working.isPackingEnabled()) {
+ working.setPackingEnabled(false);
+ }
+ checkSizes();
mergeUnpacked();
}
}
@@ -53,9 +69,6 @@ public class StructureMerger extends DataTypeMerger {
}
private void mergeUnpacked() throws DataTypeMergeException {
- if (other.isPackingEnabled()) {
- warning("Merging packed structure into an unpacked structure.");
- }
DataTypeComponent[] otherComponents = other.getDefinedComponents();
for (DataTypeComponent comp : otherComponents) {
@@ -73,8 +86,28 @@ public class StructureMerger extends DataTypeMerger {
private void copyCompToWorking(DataTypeComponent comp) throws DataTypeMergeException {
int offset = comp.getOffset();
int length = comp.getLength();
-
DataTypeComponent workingComp = working.getComponentAt(offset);
+
+ // zero length items can be added as long as it isn't in the middle of some exiting entry
+ if (length == 0) {
+ if (workingComp == null) {
+ error("Conflict at offset " + offset +
+ ". Existing component extends to this offset.");
+ }
+ else {
+ DataType dt = comp.getDataType();
+ String name = comp.getFieldName();
+ String comment = comp.getComment();
+ working.insertAtOffset(offset, dt, length, name, comment);
+ }
+ return;
+ }
+
+ if (isBitField(comp) && isBitField(workingComp)) {
+ tryMergingBitField(comp);
+ return;
+ }
+
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
@@ -94,6 +127,42 @@ public class StructureMerger extends DataTypeMerger {
}
}
+ private void tryMergingBitField(DataTypeComponent candidate) throws DataTypeMergeException {
+ BitFieldDataType dt = (BitFieldDataType) candidate.getDataType();
+ int offset = candidate.getOffset();
+ List comps = working.getComponentsContaining(offset);
+ for (DataTypeComponent comp : comps) {
+ if (!(comp.getDataType() instanceof BitFieldDataType bfdt)) {
+ error(
+ "Conflict at offset " + offset + ". Existing component exists at this offset.");
+ return;
+ }
+ if (BitFieldDataType.intersects(dt, bfdt, 0, 0)) {
+ error(
+ "Conflict at offset " + offset +
+ ". Conflicting bit field exists at this offset.");
+ return;
+ }
+ }
+ int length = candidate.getLength();
+ int bitStart = dt.getBitOffset();
+ int bitLength = dt.getBitSize();
+ String name = candidate.getFieldName();
+ String comment = candidate.getComment();
+
+ DataType baseDt = dt.getBaseDataType();
+ try {
+ working.insertBitFieldAt(offset, length, bitStart, baseDt, bitLength, name, comment);
+ }
+ catch (InvalidDataTypeException e) {
+ error("Unexpected error merging bit field: " + e.getMessage());
+ }
+ }
+
+ private boolean isBitField(DataTypeComponent comp) {
+ return comp.getDataType() instanceof BitFieldDataType;
+ }
+
private void tryMergingDataTypes(DataTypeComponent workingComp, DataTypeComponent comp)
throws DataTypeMergeException {
DataType workingDt = workingComp.getDataType();
@@ -103,6 +172,7 @@ public class StructureMerger extends DataTypeMerger {
if (mergedDt == null) {
error("Conflict at offset " + comp.getOffset() +
". Incompatible datatype already defined here.");
+ return;
}
int offset = workingComp.getOffset();
warning("Merging '%s' and '%s' at offset %d to '%s'.".formatted(workingDt.getName(),
@@ -110,7 +180,10 @@ public class StructureMerger extends DataTypeMerger {
processFieldNames(workingComp, comp); // checks for conflicts and handles null field names
String name = workingComp.getFieldName();
- String comment = join(workingComp.getComment(), comp.getComment());
+ String comment = workingComp.getComment();
+ if (StringUtils.isBlank(comment)) {
+ comment = comp.getComment();
+ }
int length = workingComp.getLength();
working.replaceAtOffset(offset, mergedDt, length, name, comment);
}
@@ -127,7 +200,6 @@ public class StructureMerger extends DataTypeMerger {
private DataTypeComponent findCorrespondingResultComponent(DataTypeComponent comp) {
int offset = comp.getOffset();
- List otherComps = other.getComponentsContaining(offset);
List workingComps = working.getComponentsContaining(offset);
if (workingComps.isEmpty()) {
@@ -138,24 +210,51 @@ public class StructureMerger extends DataTypeMerger {
return null;
}
- if (otherComps.size() == workingComps.size()) {
- int index = otherComps.indexOf(comp);
- DataTypeComponent workingComp = workingComps.get(index);
- if (isSameComponent(comp, workingComp)) {
- return workingComp;
+ for (DataTypeComponent dtc : workingComps) {
+ if (isSameComponent(comp, dtc)) {
+ return dtc;
}
}
+
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;
+ if (comp.getLength() > 0) {
+ return true;
+ }
+ // zero length dts must also have the same name to be considered the same component
+ return Objects.equals(comp.getFieldName(), workingComp.getFieldName());
+ }
+
+ private boolean canMergePacked() 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()) {
+ return false;
+ }
+
+ DataTypeComponent[] otherComps = other.getComponents();
+ DataTypeComponent[] workingComps = working.getComponents();
+ if (otherComps.length != workingComps.length) {
+ return false;
+ }
+ for (int i = 0; i < otherComps.length; i++) {
+ DataTypeComponent fromComp = otherComps[i];
+ DataTypeComponent workingComp = workingComps[i];
+ if (!checkDataType(workingComp, fromComp) || !checkOffsets(workingComp, fromComp)) {
+ return false;
+ }
+ }
+ return true; // all datatypes match up
}
private void mergePacked() throws DataTypeMergeException {
@@ -163,26 +262,23 @@ public class StructureMerger extends DataTypeMerger {
// 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.");
+ return;
}
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);
+ if (checkDataType(workingComp, fromComp) && checkOffsets(workingComp, fromComp)) {
+ processFieldNames(workingComp, fromComp);
+ processComments(workingComp, fromComp);
+ }
}
}
- private void checkDataType(DataTypeComponent workingComp, DataTypeComponent comp)
+ private boolean checkDataType(DataTypeComponent workingComp, DataTypeComponent comp)
throws DataTypeMergeException {
DataType resultDt = workingComp.getDataType();
@@ -190,17 +286,21 @@ public class StructureMerger extends DataTypeMerger {
if (!resultDt.equals(dt)) {
error("Packed components have conflicting datatypes at ordinal " +
workingComp.getOrdinal() + ", offset " + comp.getOffset());
+ return false;
}
+ return true;
}
- private void checkOffsets(DataTypeComponent comp1, DataTypeComponent comp2)
+ private boolean 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);
+ return false;
}
+ return true;
}
private void processFieldNames(DataTypeComponent workingComp, DataTypeComponent otherComp)
@@ -218,10 +318,20 @@ public class StructureMerger extends DataTypeMerger {
}
}
+ @Override
+ protected void error(String message) throws DataTypeMergeException {
+ if (terminateOnError) {
+ super.error(message);
+ }
+ errors.add(message);
+ }
+
private void processComments(DataTypeComponent workingComp, DataTypeComponent otherComp) {
- String resultComment = workingComp.getComment();
+ String workingComment = workingComp.getComment();
String otherComment = otherComp.getComment();
- workingComp.setComment(join(resultComment, otherComment));
+ if (StringUtils.isBlank(workingComment)) {
+ workingComp.setComment(otherComment);
+ }
}
}
diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/BitFieldDataType.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/BitFieldDataType.java
index 0c3c439890..40aa7e5cd3 100644
--- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/BitFieldDataType.java
+++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/BitFieldDataType.java
@@ -19,6 +19,7 @@ import java.math.BigInteger;
import java.util.*;
import ghidra.docking.settings.*;
+import ghidra.program.model.data.Structure.BitOffsetComparator;
import ghidra.program.model.mem.MemBuffer;
import ghidra.program.model.scalar.Scalar;
import ghidra.util.DataConverter;
@@ -453,4 +454,48 @@ public class BitFieldDataType extends AbstractDataType {
public String toString() {
return getDisplayName() + "(storage:" + storageSize + ",bitOffset:" + bitOffset + ")";
}
+
+ /**
+ * Checks if two bitfields would conflict if inserted into the same structure at the given
+ * offsets.
+ * @param bitFieldDataType1 the first BitFieldDataType
+ * @param bitFieldDataType2 the second BitFieldDataType
+ * @param offset1 the offset in the structure for the first BitFieldDataType
+ * @param offset2 the offset in the structure for the second BitFieldDataType
+ * @return true if the two bitFields would overlap if inserted into the same structure at
+ * the given offsets
+ */
+ public static boolean intersects(BitFieldDataType bitFieldDataType1,
+ BitFieldDataType bitFieldDataType2, int offset1, int offset2) {
+ int bitStart1 = getNormalizedBitOffset(bitFieldDataType1, offset1);
+ int bitStart2 = getNormalizedBitOffset(bitFieldDataType2, offset2);
+ int bitSize1 = bitFieldDataType1.getBitSize();
+ int bitSize2 = bitFieldDataType2.getBitSize();
+
+ if (bitStart1 < bitStart2) {
+ return bitStart1 + bitSize1 > bitStart2;
+ }
+ return bitStart2 + bitSize2 > bitStart1;
+
+ }
+
+ /**
+ * Returns the bit offset relative to the start of a structure. This is useful for
+ * determining if two bit fields can fit without conflict in the same structure.
+ * @param bitFieldDataType the {@link BitFieldDataType}
+ * @param byteOffset the offset of the component (relative to the structure struct of the
+ * component containing this BitFieldDataType.
+ *
+ * @return the bit offset relative to the start of a structure.
+ */
+ public static int getNormalizedBitOffset(BitFieldDataType bitFieldDataType, int byteOffset) {
+ boolean isBigEndean = bitFieldDataType.getDataOrganization().isBigEndian();
+ int bitSize = bitFieldDataType.getBitSize();
+ int bitOffset = bitFieldDataType.getBitOffset();
+ DataType baseDataType = bitFieldDataType.getBaseDataType();
+ int baseDtSize = baseDataType.getLength();
+ int effectiveBitSize = BitFieldDataType.getEffectiveBitSize(bitSize, baseDtSize);
+ return BitOffsetComparator.getNormalizedBitfieldOffset(byteOffset,
+ bitFieldDataType.getStorageSize(), effectiveBitSize, bitOffset, isBigEndean);
+ }
}
diff --git a/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/database/data/merge/StructureBuilder.java b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/database/data/merge/StructureBuilder.java
new file mode 100644
index 0000000000..7aecf5b7d7
--- /dev/null
+++ b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/database/data/merge/StructureBuilder.java
@@ -0,0 +1,80 @@
+/* ###
+ * 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.*;
+
+/**
+ * Convenience class for building structures in tests.
+ */
+public class StructureBuilder {
+ Structure result;
+ private DataTypeManager dtm;
+
+ public StructureBuilder(String name, int size) {
+ this(null, name, size);
+ }
+
+ public StructureBuilder(DataTypeManager dtm, String name, int size) {
+ this.dtm = dtm;
+ result = new StructureDataType(name, size, dtm);
+ }
+
+ public StructureBuilder add(int offset, DataType dt, String name) {
+ result.replaceAtOffset(offset, dt, -1, name, null);
+ return this;
+ }
+
+ public StructureBuilder add(int offset, DataType dt, String name, String comment) {
+ result.replaceAtOffset(offset, dt, -1, name, comment);
+ return this;
+ }
+
+ public StructureBuilder bitField(int offset, int byteWidth, int bitStart, int bitEnd,
+ String name, String comment) throws InvalidDataTypeException {
+ result.insertBitFieldAt(offset, byteWidth, bitStart, new IntegerDataType(),
+ bitEnd - bitStart + 1,
+ name, comment);
+ return this;
+ }
+
+ public StructureBuilder bitField(int offset, int byteWidth, int bitStart, int bitEnd,
+ String name) throws InvalidDataTypeException {
+ result.insertBitFieldAt(offset, byteWidth, bitStart, new IntegerDataType(),
+ bitEnd - bitStart + 1,
+ name, null);
+ return this;
+ }
+
+ public StructureBuilder description(String description) {
+ result.setDescription(description);
+ return this;
+ }
+
+ public Structure build() {
+ return result;
+ }
+
+ public Structure buildDb() {
+ return (Structure) dtm.resolve(result, null);
+ }
+
+ public StructureBuilder pack() {
+ result.setPackingEnabled(true);
+ return this;
+ }
+
+}
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
index ef74796220..bf54a3e70d 100644
--- 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
@@ -29,7 +29,8 @@ public class StructureMergerTest extends AbstractGenericTest {
private DataType wordDt;
private DataType dwordDt;
private IntegerDataType intDt;
- private StandAloneDataTypeManager dataTypeManager;
+ private DataType zeroArray;
+ private StandAloneDataTypeManager dtm;
private int txId;
@Before
@@ -38,28 +39,29 @@ public class StructureMergerTest extends AbstractGenericTest {
wordDt = new WordDataType();
dwordDt = new DWordDataType();
intDt = new IntegerDataType();
- dataTypeManager = new StandAloneDataTypeManager("Test");
- txId = dataTypeManager.startTransaction("Test");
+ zeroArray = new ArrayDataType(intDt, 0);
+ dtm = new StandAloneDataTypeManager("Test");
+ txId = dtm.startTransaction("Test");
}
@After
public void tearDown() {
- dataTypeManager.endTransaction(txId, false);
+ dtm.endTransaction(txId, false);
}
@Test
public void testSimpleMerge() throws Exception {
- Structure struct1 = new StructBuilder("A", 8)
- .entry(0, wordDt, "joe")
+ Structure struct1 = new StructureBuilder(dtm, "A", 8)
+ .add(0, wordDt, "joe")
.buildDb();
- Structure struct2 = new StructBuilder("B", 8)
- .entry(4, wordDt, "bob")
+ Structure struct2 = new StructureBuilder(dtm, "B", 8)
+ .add(4, wordDt, "bob")
.buildDb();
- Structure expected = new StructBuilder("A", 8)
- .entry(0, wordDt, "joe")
- .entry(4, wordDt, "bob")
+ Structure expected = new StructureBuilder(dtm, "A", 8)
+ .add(0, wordDt, "joe")
+ .add(4, wordDt, "bob")
.build();
StructureMerger merger = new StructureMerger(struct1, struct2);
@@ -70,17 +72,17 @@ public class StructureMergerTest extends AbstractGenericTest {
@Test
public void testSimpleMerge_NoDb() throws Exception {
- Structure struct1 = new StructBuilder("A", 8)
- .entry(0, wordDt, "joe")
+ Structure struct1 = new StructureBuilder(dtm, "A", 8)
+ .add(0, wordDt, "joe")
.build();
- Structure struct2 = new StructBuilder("B", 8)
- .entry(4, wordDt, "bob")
+ Structure struct2 = new StructureBuilder(dtm, "B", 8)
+ .add(4, wordDt, "bob")
.build();
- Structure expected = new StructBuilder("A", 8)
- .entry(0, wordDt, "joe")
- .entry(4, wordDt, "bob")
+ Structure expected = new StructureBuilder(dtm, "A", 8)
+ .add(0, wordDt, "joe")
+ .add(4, wordDt, "bob")
.build();
StructureMerger merger = new StructureMerger(struct1, struct2);
@@ -91,12 +93,12 @@ public class StructureMergerTest extends AbstractGenericTest {
@Test
public void testNameCollision() {
- Structure struct1 = new StructBuilder("A", 8)
- .entry(4, wordDt, "joe")
+ Structure struct1 = new StructureBuilder(dtm, "A", 8)
+ .add(4, wordDt, "joe")
.buildDb();
- Structure struct2 = new StructBuilder("B", 8)
- .entry(4, wordDt, "bob")
+ Structure struct2 = new StructureBuilder(dtm, "B", 8)
+ .add(4, wordDt, "bob")
.buildDb();
StructureMerger merger = new StructureMerger(struct1, struct2);
@@ -113,17 +115,17 @@ public class StructureMergerTest extends AbstractGenericTest {
@Test
public void testMerge_DifferentSizes() throws Exception {
- Structure struct1 = new StructBuilder("A", 4)
- .entry(0, wordDt, "joe")
+ Structure struct1 = new StructureBuilder(dtm, "A", 4)
+ .add(0, wordDt, "joe")
.build();
- Structure struct2 = new StructBuilder("B", 12)
- .entry(4, wordDt, "bob")
+ Structure struct2 = new StructureBuilder(dtm, "B", 12)
+ .add(4, wordDt, "bob")
.build();
- Structure expected = new StructBuilder("A", 12)
- .entry(0, wordDt, "joe")
- .entry(4, wordDt, "bob")
+ Structure expected = new StructureBuilder(dtm, "A", 12)
+ .add(0, wordDt, "joe")
+ .add(4, wordDt, "bob")
.build();
StructureMerger merger = new StructureMerger(struct1, struct2);
@@ -139,12 +141,12 @@ public class StructureMergerTest extends AbstractGenericTest {
@Test
public void testOverlappingFields_otherInsertsIntoMiddleOfExisting() {
- Structure struct1 = new StructBuilder("A", 8)
- .entry(0, dwordDt, "joe")
+ Structure struct1 = new StructureBuilder(dtm, "A", 8)
+ .add(0, dwordDt, "joe")
.buildDb();
- Structure struct2 = new StructBuilder("B", 8)
- .entry(3, wordDt, "bob")
+ Structure struct2 = new StructureBuilder(dtm, "B", 8)
+ .add(3, wordDt, "bob")
.buildDb();
StructureMerger merger = new StructureMerger(struct1, struct2);
@@ -160,12 +162,12 @@ public class StructureMergerTest extends AbstractGenericTest {
@Test
public void testOverlappingFields_NotEnoughRoom() {
- Structure struct1 = new StructBuilder("A", 8)
- .entry(3, dwordDt, "joe")
+ Structure struct1 = new StructureBuilder(dtm, "A", 8)
+ .add(3, dwordDt, "joe")
.buildDb();
- Structure struct2 = new StructBuilder("B", 8)
- .entry(0, dwordDt, "bob")
+ Structure struct2 = new StructureBuilder(dtm, "B", 8)
+ .add(0, dwordDt, "bob")
.buildDb();
StructureMerger merger = new StructureMerger(struct1, struct2);
@@ -181,44 +183,16 @@ public class StructureMergerTest extends AbstractGenericTest {
@Test
public void testDefinedFieldNameOverridesDefaultFieldName() throws DataTypeMergeException {
- Structure struct1 = new StructBuilder("A", 8)
- .entry(0, dwordDt, null)
+ Structure struct1 = new StructureBuilder(dtm, "A", 8)
+ .add(0, dwordDt, null)
.buildDb();
- Structure struct2 = new StructBuilder("B", 8)
- .entry(0, dwordDt, "bob")
+ Structure struct2 = new StructureBuilder(dtm, "B", 8)
+ .add(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")
+ Structure expected = new StructureBuilder(dtm, "A", 8)
+ .add(0, dwordDt, "bob")
.build();
StructureMerger merger = new StructureMerger(struct1, struct2);
@@ -228,16 +202,16 @@ public class StructureMergerTest extends AbstractGenericTest {
@Test
public void testUpgradeFromUndefined4ToDWord() throws DataTypeMergeException {
- Structure struct1 = new StructBuilder("A", 8)
- .entry(0, new Undefined4DataType(), "bob", "aaa")
+ Structure struct1 = new StructureBuilder(dtm, "A", 8)
+ .add(0, new Undefined4DataType(), "bob", "aaa")
.buildDb();
- Structure struct2 = new StructBuilder("B", 8)
- .entry(0, dwordDt, "bob", "aaa")
+ Structure struct2 = new StructureBuilder(dtm, "B", 8)
+ .add(0, dwordDt, "bob", "aaa")
.buildDb();
- Structure expected = new StructBuilder("A", 8)
- .entry(0, dwordDt, "bob", "aaa")
+ Structure expected = new StructureBuilder(dtm, "A", 8)
+ .add(0, dwordDt, "bob", "aaa")
.build();
StructureMerger merger = new StructureMerger(struct1, struct2);
@@ -253,16 +227,16 @@ public class StructureMergerTest extends AbstractGenericTest {
@Test
public void testUpgradeFromUndefined4ToDWord_otherDirection() throws DataTypeMergeException {
- Structure struct1 = new StructBuilder("A", 8)
- .entry(0, dwordDt, "bob", "aaa")
+ Structure struct1 = new StructureBuilder(dtm, "A", 8)
+ .add(0, dwordDt, "bob", "aaa")
.buildDb();
- Structure struct2 = new StructBuilder("B", 8)
- .entry(0, new Undefined4DataType(), "bob", "aaa")
+ Structure struct2 = new StructureBuilder(dtm, "B", 8)
+ .add(0, new Undefined4DataType(), "bob", "aaa")
.buildDb();
- Structure expected = new StructBuilder("A", 8)
- .entry(0, dwordDt, "bob", "aaa")
+ Structure expected = new StructureBuilder(dtm, "A", 8)
+ .add(0, dwordDt, "bob", "aaa")
.build();
StructureMerger merger = new StructureMerger(struct1, struct2);
@@ -278,17 +252,17 @@ public class StructureMergerTest extends AbstractGenericTest {
@Test
public void testUpgradeFromDWordToPointer() throws DataTypeMergeException {
- Structure struct1 = new StructBuilder("A", 8)
- .entry(0, dwordDt, "bob", "aaa")
+ Structure struct1 = new StructureBuilder(dtm, "A", 8)
+ .add(0, dwordDt, "bob", "aaa")
.buildDb();
PointerDataType pointer = new PointerDataType(intDt);
- Structure struct2 = new StructBuilder("B", 8)
- .entry(0, pointer, "bob", "aaa")
+ Structure struct2 = new StructureBuilder(dtm, "B", 8)
+ .add(0, pointer, "bob", "aaa")
.buildDb();
- Structure expected = new StructBuilder("A", 8)
- .entry(0, pointer, "bob", "aaa")
+ Structure expected = new StructureBuilder(dtm, "A", 8)
+ .add(0, pointer, "bob", "aaa")
.build();
StructureMerger merger = new StructureMerger(struct1, struct2);
@@ -302,17 +276,17 @@ public class StructureMergerTest extends AbstractGenericTest {
@Test
public void testUpgradePointers() throws DataTypeMergeException {
- Structure struct1 = new StructBuilder("A", 8)
- .entry(0, new PointerDataType(new Undefined4DataType()), "bob", "aaa")
+ Structure struct1 = new StructureBuilder(dtm, "A", 8)
+ .add(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")
+ Structure struct2 = new StructureBuilder(dtm, "B", 8)
+ .add(0, new PointerDataType(new IntegerDataType()), "bob", "aaa")
.buildDb();
- Structure expected = new StructBuilder("A", 8)
- .entry(0, new PointerDataType(new IntegerDataType()), "bob", "aaa")
+ Structure expected = new StructureBuilder(dtm, "A", 8)
+ .add(0, new PointerDataType(new IntegerDataType()), "bob", "aaa")
.build();
StructureMerger merger = new StructureMerger(struct1, struct2);
@@ -326,18 +300,18 @@ public class StructureMergerTest extends AbstractGenericTest {
@Test
public void testPackedStructureSameExceptFieldName() throws DataTypeMergeException {
- Structure struct1 = new StructBuilder("A", 8)
- .entry(0, dwordDt, null)
+ Structure struct1 = new StructureBuilder(dtm, "A", 8)
+ .add(0, dwordDt, null)
.pack()
.buildDb();
- Structure struct2 = new StructBuilder("B", 8)
- .entry(0, dwordDt, "bob")
+ Structure struct2 = new StructureBuilder(dtm, "B", 8)
+ .add(0, dwordDt, "bob")
.pack()
.buildDb();
- Structure expected = new StructBuilder("A", 8)
- .entry(0, dwordDt, "bob")
+ Structure expected = new StructureBuilder(dtm, "A", 8)
+ .add(0, dwordDt, "bob")
.pack()
.build();
@@ -347,44 +321,38 @@ public class StructureMergerTest extends AbstractGenericTest {
}
@Test
- public void testPackedStructureDifferentSize() {
- Structure struct1 = new StructBuilder("A", 8)
- .entry(0, dwordDt, null)
+ public void testPackedStructureDifferentSize() throws DataTypeMergeException {
+ Structure struct1 = new StructureBuilder(dtm, "A", 8)
+ .add(0, dwordDt, null)
.pack()
.buildDb();
- Structure struct2 = new StructBuilder("B", 8)
- .entry(0, dwordDt, "bob")
- .entry(4, dwordDt, "joe")
+ Structure struct2 = new StructureBuilder(dtm, "B", 8)
+ .add(0, dwordDt, "bob")
+ .add(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());
- }
+
+ Structure result = merger.merge();
+ assertFalse(result.isPackingEnabled());
}
@Test
public void testMergingPackedIntoUnpacked() throws DataTypeMergeException {
- Structure struct1 = new StructBuilder("A", 8)
- .entry(4, dwordDt, "joe")
+ Structure struct1 = new StructureBuilder(dtm, "A", 8)
+ .add(4, dwordDt, "joe")
.buildDb();
- Structure struct2 = new StructBuilder("B", 8)
- .entry(0, dwordDt, "bob")
+ Structure struct2 = new StructureBuilder(dtm, "B", 8)
+ .add(0, dwordDt, "bob")
.pack()
.buildDb();
- Structure expected = new StructBuilder("A", 8)
- .entry(0, dwordDt, "bob")
- .entry(4, dwordDt, "joe")
+ Structure expected = new StructureBuilder(dtm, "A", 8)
+ .add(0, dwordDt, "bob")
+ .add(4, dwordDt, "joe")
.build();
StructureMerger merger = new StructureMerger(struct1, struct2);
@@ -394,19 +362,19 @@ public class StructureMergerTest extends AbstractGenericTest {
@Test
public void testMergeCyclic() throws Exception {
- Structure struct1 = new StructBuilder("A", 16)
- .entry(0, wordDt, "joe")
+ Structure struct1 = new StructureBuilder(dtm, "A", 16)
+ .add(0, wordDt, "joe")
.buildDb();
- Structure struct2 = new StructBuilder("B", 16)
- .entry(4, wordDt, "bob")
- .entry(10, new PointerDataType(struct1), "ptr2")
+ Structure struct2 = new StructureBuilder(dtm, "B", 16)
+ .add(4, wordDt, "bob")
+ .add(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")
+ Structure expected = new StructureBuilder(dtm, "A", 16)
+ .add(0, wordDt, "joe")
+ .add(4, wordDt, "bob")
.build();
expected.replaceAtOffset(6, new PointerDataType(expected), 4, "ptr1", null);
expected.replaceAtOffset(10, new PointerDataType(expected), 4, "ptr2", null);
@@ -416,25 +384,25 @@ public class StructureMergerTest extends AbstractGenericTest {
// we need to do the replace with to complete the cycle
struct1.replaceWith(result);
- dataTypeManager.replaceDataType(struct2, struct1, false);
+ dtm.replaceDataType(struct2, struct1, false);
assertStructEquals(expected, struct1);
}
@Test
public void testMergeSameDescription() throws DataTypeMergeException {
- Structure struct1 = new StructBuilder("A", 16)
- .entry(0, wordDt, "joe")
+ Structure struct1 = new StructureBuilder(dtm, "A", 16)
+ .add(0, wordDt, "joe")
.description("Hi")
.buildDb();
- Structure struct2 = new StructBuilder("B", 16)
- .entry(4, wordDt, "bob")
+ Structure struct2 = new StructureBuilder(dtm, "B", 16)
+ .add(4, wordDt, "bob")
.description("Hi")
.buildDb();
- Structure expected = new StructBuilder("A", 16)
- .entry(0, wordDt, "joe")
- .entry(4, wordDt, "bob")
+ Structure expected = new StructureBuilder(dtm, "A", 16)
+ .add(0, wordDt, "joe")
+ .add(4, wordDt, "bob")
.description("Hi")
.build();
@@ -447,19 +415,19 @@ public class StructureMergerTest extends AbstractGenericTest {
@Test
public void testMergeDifferentDescription() throws DataTypeMergeException {
- Structure struct1 = new StructBuilder("A", 16)
- .entry(0, wordDt, "joe")
+ Structure struct1 = new StructureBuilder(dtm, "A", 16)
+ .add(0, wordDt, "joe")
.description("Hi")
.buildDb();
- Structure struct2 = new StructBuilder("B", 16)
- .entry(4, wordDt, "bob")
+ Structure struct2 = new StructureBuilder(dtm, "B", 16)
+ .add(4, wordDt, "bob")
.description("There")
.buildDb();
- Structure expected = new StructBuilder("A", 16)
- .entry(0, wordDt, "joe")
- .entry(4, wordDt, "bob")
+ Structure expected = new StructureBuilder(dtm, "A", 16)
+ .add(0, wordDt, "joe")
+ .add(4, wordDt, "bob")
.description("Hi There")
.build();
@@ -470,6 +438,164 @@ public class StructureMergerTest extends AbstractGenericTest {
assertEquals("Hi There", expected.getDescription());
}
+ @Test
+ public void testDatatypeCollisionNonTerminate() throws DataTypeMergeException {
+ Structure struct1 = new StructureBuilder(dtm, "A", 8)
+ .add(0, wordDt, "joe")
+ .buildDb();
+
+ Structure struct2 = new StructureBuilder(dtm, "B", 8)
+ .add(0, intDt, "bob")
+ .add(4, wordDt, "cal")
+ .buildDb();
+
+ Structure expected = new StructureBuilder(dtm, "A", 8)
+ .add(0, wordDt, "joe")
+ .add(4, wordDt, "cal")
+ .build();
+
+ StructureMerger merger = new StructureMerger(struct1, struct2, false);
+ Structure result = merger.merge();
+
+ assertStructEquals(expected, result);
+ }
+
+ @Test
+ public void testDatatypeUpgradeNamesDifferNonTerminate() throws DataTypeMergeException {
+ Structure struct1 = new StructureBuilder(dtm, "A", 8)
+ .add(0, new Undefined4DataType(), "joe")
+ .buildDb();
+
+ Structure struct2 = new StructureBuilder(dtm, "B", 8)
+ .add(0, intDt, "bob")
+ .add(4, wordDt, "cal")
+ .buildDb();
+
+ Structure expected = new StructureBuilder(dtm, "A", 8)
+ .add(0, intDt, "joe")
+ .add(4, wordDt, "cal")
+ .build();
+
+ StructureMerger merger = new StructureMerger(struct1, struct2, false);
+ Structure result = merger.merge();
+
+ assertStructEquals(expected, result);
+ }
+
+ @Test
+ public void testNameCollisionNonTerminate() throws DataTypeMergeException {
+ Structure struct1 = new StructureBuilder(dtm, "A", 8)
+ .add(0, wordDt, "joe")
+ .buildDb();
+
+ Structure struct2 = new StructureBuilder(dtm, "B", 8)
+ .add(0, wordDt, "bob")
+ .add(4, wordDt, "cal")
+ .buildDb();
+
+ Structure expected = new StructureBuilder(dtm, "A", 8)
+ .add(0, wordDt, "joe")
+ .add(4, wordDt, "cal")
+ .build();
+
+ StructureMerger merger = new StructureMerger(struct1, struct2, false);
+ Structure result = merger.merge();
+
+ assertStructEquals(expected, result);
+ }
+
+ @Test
+ public void testZeroLengthArraysNoConflict() throws DataTypeMergeException {
+ Structure struct1 = new StructureBuilder(dtm, "A", 8)
+ .add(0, wordDt, "joe")
+ .buildDb();
+
+ Structure struct2 = new StructureBuilder(dtm, "B", 8)
+ .add(0, zeroArray, "bob")
+ .add(0, zeroArray, "cal")
+ .add(4, wordDt, "dave")
+ .buildDb();
+
+ Structure expected = new StructureBuilder(dtm, "A", 8)
+ .add(0, zeroArray, "bob")
+ .add(0, zeroArray, "cal")
+ .add(0, wordDt, "joe")
+ .add(4, wordDt, "dave")
+ .build();
+
+ StructureMerger merger = new StructureMerger(struct1, struct2, false);
+ Structure result = merger.merge();
+
+ assertStructEquals(expected, result);
+ }
+
+ @Test
+ public void testZeroLengthArraysWithConflict() throws DataTypeMergeException {
+ Structure struct1 = new StructureBuilder(dtm, "A", 8)
+ .add(0, wordDt, "joe")
+ .buildDb();
+
+ Structure struct2 = new StructureBuilder(dtm, "B", 8)
+ .add(1, zeroArray, "bob")
+ .add(4, wordDt, "dave")
+ .buildDb();
+
+ StructureMerger merger = new StructureMerger(struct1, struct2);
+ try {
+ merger.merge();
+ fail("Expected error for offcut collision");
+ }
+ catch (DataTypeMergeException e) {
+ assertEquals("Conflict at offset 1. Existing component extends to this offset.",
+ e.getMessage());
+ }
+ }
+
+ @Test
+ public void testBiFieldsNoConflict() throws Exception {
+ Structure struct1 = new StructureBuilder(dtm, "A", 8)
+ .bitField(0, 1, 0, 2, "A")
+ .bitField(0, 1, 6, 7, "B")
+ .buildDb();
+
+ Structure struct2 = new StructureBuilder(dtm, "B", 8)
+ .bitField(0, 1, 3, 5, "C")
+ .buildDb();
+
+ Structure expected = new StructureBuilder(dtm, "A", 8)
+ .bitField(0, 1, 0, 2, "A")
+ .bitField(0, 1, 3, 5, "C")
+ .bitField(0, 1, 6, 7, "B")
+ .build();
+
+ StructureMerger merger = new StructureMerger(struct1, struct2, false);
+ Structure result = merger.merge();
+
+ assertStructEquals(expected, result);
+ }
+
+ @Test
+ public void testBiFieldsWithConflict() throws Exception {
+ Structure struct1 = new StructureBuilder(dtm, "A", 8)
+ .bitField(0, 1, 0, 2, "A")
+ .bitField(0, 1, 6, 7, "B")
+ .buildDb();
+
+ Structure struct2 = new StructureBuilder(dtm, "B", 8)
+ .bitField(0, 1, 1, 7, "C")
+ .buildDb();
+
+ StructureMerger merger = new StructureMerger(struct1, struct2);
+ try {
+ merger.merge();
+ fail("Expected error for offcut collision");
+ }
+ catch (DataTypeMergeException e) {
+ assertEquals("Conflict at offset 0. Conflicting bit field exists at this offset.",
+ e.getMessage());
+ }
+ }
+
private void assertStructEquals(Structure expected, Structure actual) {
if (expected.equals(actual)) {
return;
@@ -480,41 +606,4 @@ public class StructureMergerTest extends AbstractGenericTest {
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/Utility/src/main/java/utility/function/Dummy.java b/Ghidra/Framework/Utility/src/main/java/utility/function/Dummy.java
index 4fa91bc668..35e5cd6a3c 100644
--- a/Ghidra/Framework/Utility/src/main/java/utility/function/Dummy.java
+++ b/Ghidra/Framework/Utility/src/main/java/utility/function/Dummy.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.
@@ -42,6 +42,16 @@ public class Dummy {
};
}
+ /**
+ * Creates a dummy consumer
+ * @return a dummy consumer
+ */
+ public static ExceptionalConsumer exceptionalConsumer() {
+ return t -> {
+ // no-op
+ };
+ }
+
/**
* Creates a dummy consumer
* @return a dummy consumer
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 290421e0a4..5dc020278d 100644
--- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataTypeManagerPluginScreenShots.java
+++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataTypeManagerPluginScreenShots.java
@@ -36,6 +36,7 @@ import ghidra.app.plugin.core.datamgr.actions.FindStructuresBySizeAction;
import ghidra.app.plugin.core.datamgr.archive.DataTypeManagerHandler;
import ghidra.app.plugin.core.datamgr.archive.InvalidFileArchive;
import ghidra.framework.preferences.Preferences;
+import ghidra.program.database.data.merge.StructureBuilder;
import ghidra.program.model.data.*;
import ghidra.util.UniversalID;
import ghidra.util.table.GhidraTable;
@@ -130,10 +131,40 @@ public class DataTypeManagerPluginScreenShots extends GhidraScreenShotGenerator
captureIsolatedProvider(DataTypesProvider.class, 500, 400);
}
+ @Test
+ public void testStructureMergeDialog() {
+ Structure structure1 = new StructureBuilder("foo", 8)
+ .add(0, new IntegerDataType(), "aaa")
+ .add(4, new IntegerDataType(), "bbb")
+ .build();
+ Structure structure2 = new StructureBuilder("bar", 8)
+ .add(0, new DWordDataType(), "aaa")
+ .add(6, new WordDataType(), "ccc")
+ .build();
+ addDataType(structure1);
+ addDataType(structure2);
+
+ 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 testMergeConfirmationDialog() {
- createStructure("foo", 0, new IntegerDataType(), "aaa", 12);
- createStructure("bar", 4, new FloatDataType(), "bbb", 16);
+ createEnum("foo", "AAAA", 6);
+ createEnum("bar", "BBBB", 2);
DataTypesProvider provider = getProvider(DataTypesProvider.class);
GTree tree = (GTree) getInstanceField("archiveGTree", provider);
@@ -154,8 +185,8 @@ public class DataTypeManagerPluginScreenShots extends GhidraScreenShotGenerator
@Test
public void testMergeErrorDialog() {
- createStructure("foo", 0, new IntegerDataType(), "aaa", 12);
- createStructure("bar", 0, new FloatDataType(), "bbb", 16);
+ createEnum("foo", "AAAA", 6);
+ createEnum("bar", "AAAA", 4);
DataTypesProvider provider = getProvider(DataTypesProvider.class);
GTree tree = (GTree) getInstanceField("archiveGTree", provider);
@@ -174,12 +205,16 @@ public class DataTypeManagerPluginScreenShots extends GhidraScreenShotGenerator
pressButtonOnDialog("OK");
}
- private void createStructure(String name, int offset, DataType dt, String fieldName, int size) {
+ private void createEnum(String name, String valueName, int value) {
+ EnumDataType enumm = new EnumDataType(name, 4);
+ enumm.add(valueName, value);
+ addDataType(enumm);
+ }
+
+ private void addDataType(DataType dt) {
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);
+ dtm.addDataType(dt, null);
});
}