Merge remote-tracking branch 'origin/GP-6453_ghidragon_structure_merge_gui'

This commit is contained in:
Ryan Kurtz
2026-04-02 13:29:22 -04:00
30 changed files with 4120 additions and 247 deletions
@@ -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|
@@ -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]
@@ -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)
@@ -1095,22 +1095,70 @@
<H3><A name="MergeDataType"></A>Merging Data Types</H3>
<BLOCKQUOTE>
<P>Some data types can be merged with others of the same type, provided there are no
conflicting entries in the two data types. Currently,
structures, unions and enums are supported for merging. If a conflict is detected,
an error dialog will be displayed showing the two data types and a message explaining
why the merge failed.</P>
<P>Some data types can be merged with others of the same type. </P>
<P>Currently, Ghidra supports
merging structures, unions, and enums. Unions and enums have limited support in that you
can only merge them if there are no conflicts and Ghidra can auto-merge them. For
structures, Ghidra will launch an interactive structure merger dialog that allows users
to pick and choose various elements from either structure.</P>
<P>To Merge a data type, right-click on the type to be merged into and select the
<B><I>Merge...</I></B> action. This will show a
<A HREF="help/topics/DataTypeEditors/DataTypeSelectionDialog.htm">dialog</A> that allows
you to choose the data type to merge with.
</P>
<H4><A name="MergeStructures">Merging Structures</H4>
<BLOCKQUOTE>
<P>When merging structures, an interactive dialog will be displayed showing the two
structures being merged along with a preview of the resulting merged structure.
</BLOCKQUOTE>
<P><CENTER>
<IMG src="images/StructureMergeDialog.png" alt="" border="0"/>
</CENTER></P>
<BLOCKQUOTE>
<P>In the upper half of the dialog, the two structures being merged are displayed side by
side and aligned by offset. The lower section displays the resulting structure which changes
as the user selects different components from the two structures being merged. All three
structure views scroll together and selecting a line in any view, will also select
the corresponding line in the other views.</P>
<P>In between the the display of the two input structures is a button panel that displays
buttons on any lines where there is a conflict and the user has a choice to select the
values from the left or right side structure.</P>
<P>The displays of the left/right input structures will have optional values bolded or
faded, depending on if that value is currently chosen to be included in the resulting
merged structure.</P>
<P>Selecting a button will cause that side's corresponding values to be applied to the
resulting structure and will cause any conflicting components from the other side to
be removed.</P>
<P>Buttons associated with structure component lines can also be deselected, clearing
the corresponding component from the merged structure.</P>
<P>The choices for the structure name and description always require one side or the
other to be selected, but for component choices, it is possible also deselect a button,
causing the resulting structure to not have a value from either side, instead reverting
to undefined bytes.</P>
<P>The dialog has several features to make it easier to control from just the keyboard.
Using the tab and <shift>tab, the focus can be quickly moved between the left display,
the right display, the merged display, apply button and the cancel button. In addition,
you can quickly jump to the the left or right display by pressing the left or
right arrow keys respectively. Also, you can apply/unapply a left or right side item by
navigating to the item on the left or right side and pressing the [SPACE] key. </P>
<P>Also, you can quickly give focus to the left or right display, by pressing the
[LEFT ARROW] or [RIGHT ARROW] keys respectively. </P>
<P>When the resulting structure has been adjusted to the desired result, press the
<B>Apply</B> button to perform the merge.</P>
</BLOCKQUOTE>
<H4>Merging Enums and Unions</H4>
<BLOCKQUOTE>
<P>Merging Enums and Unions is a simple pass/fail action. Generally, enums and unions
will merge without conflicts unless one or more elements have the same name for different
values/datatypes.</P>
<P>If the merge succeeds in producing a merged data type result, a confirmation dialog
will be displayed showing the result and the two datatypes being merged. If the user
confirms the merge, the original target data type will have its internals replaced
with the merged data type and the other data type will have all its references replaced
with the resulting datatype and then it will be deleted.
</P>
<P>To Merge a data type, right-click on the type to be merged into and select the
<B><I>Merge...</I></B> action. This will show a
<A HREF="help/topics/DataTypeEditors/DataTypeSelectionDialog.htm">dialog</A> that allows
you to choose the data type to merge with.
</P>
<A name="Merge_Confirmation"></A><P>If the merge succeeds in producing a merged data type,
the following confirmation dialog will be displayed before the changes are actually
applied.</P>
@@ -1123,7 +1171,6 @@
with the merge. If the user presses the apply button, the first data type will be updated
to match the preview data type and the second data type will be deleted with all of its
uses replaced with the updated first data type.</P>
<A name="Merge_Error"></A>
<P>If the merge fails, the following error dialog is displayed showing a side-by-side
view of the two data types that couldn't be merged, along with a description of the error
@@ -1134,6 +1181,7 @@
</CENTER></P>
<BR><BR><BR>
</BLOCKQUOTE>
</BLOCKQUOTE>
<H3><A name="Favorites"></A>Setting Favorite Data Types</H3>
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@@ -0,0 +1,181 @@
/* ###
* 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;
/**
* Base class for items that can be displayed in a {@link CoordinatedStructureDisplay}. These are
* basically different views from a {@link CoordinatedStructureLine} where each coordinated line
* has three comparison items, one for the left side structure, one for the right side structure,
* and for the merged structure.
*/
public abstract class ComparisonItem implements Comparable<ComparisonItem> {
// 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;
}
}
@@ -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.
* <P> 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);
}
}
}
@@ -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<ComparisonItem>, 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<String, ColumnWidths> widthsMap;
ComparisonItemRenderer(ListModel<ComparisonItem> 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<ComparisonItem> 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<ComparisonItem>) e.getSource());
}
}
@@ -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<ComparisonItem> 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<ComparisonItem>(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;
}
}
@@ -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);
}
}
@@ -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<CoordinatedStructureLine> compareLines;
private Consumer<String> errorHandler;
private List<Callback> 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<String> 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<CoordinatedStructureLine> buildLines() {
List<CoordinatedStructureLine> 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<CoordinatedStructureLine> lines;
private StructureComponentLine lastComponentLine;
private DefinedComponentQueue q1 = new DefinedComponentQueue(leftStruct);
private DefinedComponentQueue q2 = new DefinedComponentQueue(rightStruct);
private DefinedComponentQueue q3 = new DefinedComponentQueue(mergedStruct);
LineBuilder(List<CoordinatedStructureLine> 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<DataTypeComponent> comps1 = getZeroLengthComps(q1, offset);
List<DataTypeComponent> comps2 = getZeroLengthComps(q2, offset);
List<DataTypeComponent> 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<DataTypeComponent> list,
DataTypeComponent comp) {
if (list.isEmpty()) {
return null;
}
DataType dataType = comp.getDataType();
String name = comp.getFieldName();
Iterator<DataTypeComponent> 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<DataTypeComponent> getZeroLengthComps(DefinedComponentQueue q, int offset) {
if (!q.hasZeroComp(offset)) {
return Collections.emptyList();
}
List<DataTypeComponent> 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<CoordinatedStructureLine> getLines() {
return compareLines;
}
public void addChangeListener(Callback callback) {
changeCallbacks.add(callback);
}
public List<ComparisonItem> getData(CompareId compareId) {
List<ComparisonItem> 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;
}
}
@@ -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<CoordinatedStructureDisplay> 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;
}
}
@@ -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<ComparisonItem> {
private CoordinatedStructureModel model;
private CompareId compareId;
private List<ComparisonItem> 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);
}
}
@@ -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<DataTypeComponent> 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<DataTypeComponent> 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;
}
}
}
@@ -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());
}
}
}
@@ -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;
}
}
}
@@ -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.
* <P>
* 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.
* <P>
* The dialog also provides the following actions as keyboard only actions:
* <OL>
* <LI>Apply Item (&LTSPACE&GT): 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).</LI>
* <LI>Focus Left Side (&LT;LEFT ARROW&GT;): pressing the left arrow will give focus to the left side
* display.</LI>
* <LI>Focus Right Side (&LT;RIGHT ARROW&GT;): pressing the right arrow will give focus to the right side
* display.</LI>
* </OL>
*/
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<Structure, Exception> 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<Structure, Exception> 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;
}
}
}
@@ -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;
}
}
}
@@ -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);
@@ -175,9 +175,10 @@ public abstract class DataTypeMerger<T extends DataType> {
}
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);
}
}
@@ -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<Structure> {
private boolean terminateOnError;
private List<String> 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<Structure> {
}
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<Structure> {
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<Structure> {
}
}
private void tryMergingBitField(DataTypeComponent candidate) throws DataTypeMergeException {
BitFieldDataType dt = (BitFieldDataType) candidate.getDataType();
int offset = candidate.getOffset();
List<DataTypeComponent> 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<Structure> {
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<Structure> {
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<Structure> {
private DataTypeComponent findCorrespondingResultComponent(DataTypeComponent comp) {
int offset = comp.getOffset();
List<DataTypeComponent> otherComps = other.getComponentsContaining(offset);
List<DataTypeComponent> workingComps = working.getComponentsContaining(offset);
if (workingComps.isEmpty()) {
@@ -138,24 +210,51 @@ public class StructureMerger extends DataTypeMerger<Structure> {
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<Structure> {
// 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<Structure> {
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<Structure> {
}
}
@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);
}
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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 <T, E extends Throwable> ExceptionalConsumer<T, E> exceptionalConsumer() {
return t -> {
// no-op
};
}
/**
* Creates a dummy consumer
* @return a dummy consumer
@@ -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);
});
}