From 7e8b312b78abf22bd7dca689f63b192673e2e37e Mon Sep 17 00:00:00 2001 From: James <49045138+ghidracadabra@users.noreply.github.com> Date: Fri, 13 Oct 2023 16:20:53 -0400 Subject: [PATCH] GP-2297 added action to create a table based on address ranges in a selection --- .../Base/data/base.icons.theme.properties | 2 +- .../CodeBrowserPlugin/SelectionTable.htm | 21 +- .../AddressRangeBytesTableColumn.java | 127 +++++++++ .../AddressRangeCodeUnitTableColumn.java | 109 ++++++++ .../core/codebrowser/AddressRangeInfo.java | 75 ++++++ .../codebrowser/AddressRangeTableModel.java | 254 ++++++++++++++++++ .../CodeBrowserSelectionPlugin.java | 83 +++++- .../codebrowser/SelectEndpointsAction.java | 138 ++++++++++ ...ddressRangeEndpointSettingsDefinition.java | 122 +++++++++ .../util/table/field/BytesTableColumn.java | 102 +------ .../table/field/MonospacedByteRenderer.java | 123 +++++++++ .../ghidra/app/plugin/core/totd/tips.txt | 2 + .../core/codebrowser/CodeBrowserTest.java | 53 +++- 13 files changed, 1101 insertions(+), 110 deletions(-) create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/AddressRangeBytesTableColumn.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/AddressRangeCodeUnitTableColumn.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/AddressRangeInfo.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/AddressRangeTableModel.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/SelectEndpointsAction.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/util/table/field/AddressRangeEndpointSettingsDefinition.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/util/table/field/MonospacedByteRenderer.java diff --git a/Ghidra/Features/Base/data/base.icons.theme.properties b/Ghidra/Features/Base/data/base.icons.theme.properties index 3792d7c068..9afdb5f3d0 100644 --- a/Ghidra/Features/Base/data/base.icons.theme.properties +++ b/Ghidra/Features/Base/data/base.icons.theme.properties @@ -396,4 +396,4 @@ icon.base.util.datatree.version.control.archive.dt.checkout.undo = vcUndoCheckOu -[Dark Defaults] \ No newline at end of file +[Dark Defaults] diff --git a/Ghidra/Features/Base/src/main/help/help/topics/CodeBrowserPlugin/SelectionTable.htm b/Ghidra/Features/Base/src/main/help/help/topics/CodeBrowserPlugin/SelectionTable.htm index ca7c5ba4cc..1809a36ff0 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/CodeBrowserPlugin/SelectionTable.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/CodeBrowserPlugin/SelectionTable.htm @@ -13,7 +13,7 @@ -

Selection Table

+

Selection Tables

The Create Table From Selection action will create a table of addresses based upon the current selection in the Listing. Each entry in @@ -30,6 +30,25 @@ "help/shared/arrow.gif">Create Table From Selection from the tool's menu bar.

+

The Create Table From Ranges action will create a table based on the + current selection. Each row of the table corresponds to an address range in the selection.

+ +

Range Table Actions

+ +

There are two special actions available on an address range table: +

+

Provided by: Code Browser plugin

Related Topics:

diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/AddressRangeBytesTableColumn.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/AddressRangeBytesTableColumn.java new file mode 100644 index 0000000000..c19462e7cb --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/AddressRangeBytesTableColumn.java @@ -0,0 +1,127 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.codebrowser; + +import docking.widgets.table.AbstractDynamicTableColumn; +import ghidra.docking.settings.*; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.program.model.address.*; +import ghidra.program.model.data.EndianSettingsDefinition; +import ghidra.program.model.listing.*; +import ghidra.program.model.mem.MemoryAccessException; +import ghidra.util.table.column.GColumnRenderer; +import ghidra.util.table.field.*; + +/** + * A column for displaying a small window of bytes around the endpoints of an {@link AddressRange} + * in an address range table + */ +public class AddressRangeBytesTableColumn + extends AbstractDynamicTableColumn { + + private final GColumnRenderer monospacedRenderer = new MonospacedByteRenderer(); + private static SettingsDefinition[] SETTINGS = + { ByteCountSettingsDefinition.DEF, MemoryOffsetSettingsDefinition.DEF, + EndianSettingsDefinition.DEF, FormatSettingsDefinition.DEF, + AddressRangeEndpointSettingsDefinition.DEF }; + private static final String COLUMN_NAME = "Bytes"; + + /** + * Default constructor + */ + public AddressRangeBytesTableColumn() { + } + + @Override + public String getColumnName() { + return COLUMN_NAME; + } + + @Override + public Byte[] getValue(AddressRangeInfo rowObject, Settings settings, Program program, + ServiceProvider serviceProvider) throws IllegalArgumentException { + int choice = AddressRangeEndpointSettingsDefinition.DEF.getChoice(settings); + Address base = + choice == AddressRangeEndpointSettingsDefinition.BEGIN_CHOICE_INDEX ? rowObject.min() + : rowObject.max(); + int offset = MemoryOffsetSettingsDefinition.DEF.getOffset(settings); + int byteCount = ByteCountSettingsDefinition.DEF.getChoice(settings); + byte[] bytes = null; + //default: display the bytes in the associated CodeUnit + //unless there is a nonzero offset. In that case, display the one byte at that offset + try { + base = base.addNoWrap(offset); + if (byteCount == ByteCountSettingsDefinition.DEFAULT) { + if (offset != 0) { + byteCount = 1; + } + else { + CodeUnit cu = program.getListing().getCodeUnitContaining(base); + if (cu == null) { // can happen for 'SpecialAddress'es + return new Byte[0]; + } + if (cu instanceof Instruction instr) { + bytes = instr.getParsedBytes(); + } + else { + bytes = cu.getBytes(); + } + } + } + if (bytes == null) { + bytes = new byte[byteCount]; + program.getMemory().getBytes(base, bytes); + } + + Byte[] bytesObj = new Byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + bytesObj[i] = Byte.valueOf(bytes[i]); + } + return bytesObj; + } + catch (MemoryAccessException | AddressOverflowException e) { + return new Byte[0]; + } + } + + @Override + public GColumnRenderer getColumnRenderer() { + return monospacedRenderer; + } + + @Override + public SettingsDefinition[] getSettingsDefinitions() { + return SETTINGS; + } + + @Override + public String getColumnDisplayName(Settings settings) { + StringBuilder sb = + new StringBuilder(AddressRangeEndpointSettingsDefinition.DEF.getValueString(settings)); + sb.append(" ").append(COLUMN_NAME); + int byteCnt = ByteCountSettingsDefinition.DEF.getChoice(settings); + if (byteCnt != ByteCountSettingsDefinition.DEFAULT) { + sb.append("["); + sb.append(byteCnt); + sb.append("]"); + } + String offset = MemoryOffsetSettingsDefinition.DEF.getValueString(settings); + if (!offset.equals("0")) { + sb.append(offset); + } + return sb.toString(); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/AddressRangeCodeUnitTableColumn.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/AddressRangeCodeUnitTableColumn.java new file mode 100644 index 0000000000..e04a586a39 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/AddressRangeCodeUnitTableColumn.java @@ -0,0 +1,109 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.codebrowser; + +import docking.widgets.table.AbstractDynamicTableColumn; +import ghidra.app.util.viewer.field.BrowserCodeUnitFormat; +import ghidra.docking.settings.Settings; +import ghidra.docking.settings.SettingsDefinition; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRange; +import ghidra.program.model.listing.*; +import ghidra.program.util.ProgramLocation; +import ghidra.util.table.CodeUnitTableCellRenderer; +import ghidra.util.table.column.GColumnRenderer; +import ghidra.util.table.field.*; + +/** + * A column for displaying a small window of {@link CodeUnit}s around a selected endpoint + * of an {@link AddressRange} in an address range table + */ +public class AddressRangeCodeUnitTableColumn + extends AbstractDynamicTableColumn { + + private static SettingsDefinition[] SETTINGS = { CodeUnitCountSettingsDefinition.DEF, + CodeUnitOffsetSettingsDefinition.DEF, AddressRangeEndpointSettingsDefinition.DEF }; + + private final CodeUnitTableCellRenderer renderer = new CodeUnitTableCellRenderer(); + private CodeUnitFormat codeUnitFormat; + private static final String COLUMN_NAME = "Code Unit"; + + /** + * Default constructor + */ + public AddressRangeCodeUnitTableColumn() { + } + + @Override + public String getColumnName() { + return COLUMN_NAME; + } + + @Override + public String getColumnDisplayName(Settings settings) { + StringBuilder sb = + new StringBuilder(AddressRangeEndpointSettingsDefinition.DEF.getValueString(settings)); + sb.append(" ").append(COLUMN_NAME); + int previewCnt = CodeUnitCountSettingsDefinition.DEF.getCount(settings); + if (previewCnt != 1) { + sb.append("["); + sb.append(previewCnt); + sb.append("]"); + } + String offset = CodeUnitOffsetSettingsDefinition.DEF.getDisplayValue(settings); + if (!"0".equals(offset)) { + sb.append(offset); + } + return sb.toString(); + } + + @Override + public CodeUnitTableCellData getValue(AddressRangeInfo rowObject, Settings settings, + Program program, ServiceProvider serviceProvider) throws IllegalArgumentException { + int choice = AddressRangeEndpointSettingsDefinition.DEF.getChoice(settings); + Address base = + choice == AddressRangeEndpointSettingsDefinition.BEGIN_CHOICE_INDEX ? rowObject.min() + : rowObject.max(); + ProgramLocation location = new ProgramLocation(program, base); + return new CodeUnitTableCellData(location, getCodeUnitFormat(serviceProvider), + CodeUnitOffsetSettingsDefinition.DEF.getOffset(settings), + CodeUnitCountSettingsDefinition.DEF.getCount(settings)); + } + + @Override + public GColumnRenderer getColumnRenderer() { + return renderer; + } + + @Override + public SettingsDefinition[] getSettingsDefinitions() { + return SETTINGS; + } + + @Override + public int getMaxLines(Settings settings) { + return CodeUnitCountSettingsDefinition.DEF.getCount(settings); + } + + private CodeUnitFormat getCodeUnitFormat(ServiceProvider serviceProvider) { + if (codeUnitFormat == null) { + codeUnitFormat = new BrowserCodeUnitFormat(serviceProvider); + } + return codeUnitFormat; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/AddressRangeInfo.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/AddressRangeInfo.java new file mode 100644 index 0000000000..a2a83523cf --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/AddressRangeInfo.java @@ -0,0 +1,75 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.codebrowser; + +import java.util.Objects; + +import ghidra.program.model.address.*; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.MemoryAccessException; + +/** + * A record for information about an {@link AddressRange}, used when creating + * address range tables + * + * @param min smallest address in range + * @param max largest address in range + * @param size number of addresses in range + * @param isSameByte true precisely when all of the bytes in the range have the same value or + * all are undefined + * @param numRefsTo number of references to this range + * @param numRefsFrom number of references out of this range + */ +public record AddressRangeInfo(Address min, Address max, long size, boolean isSameByte, + int numRefsTo, int numRefsFrom) { + + /** + * Returns true precisely when all of the addresses between min and max (inclusive) + * have the same byte value OR all addresses are without values + * + * @param min minimum address + * @param max maximum address + * @param program program + * @return true if all addresses have same value + */ + public static boolean isSameByteValue(Address min, Address max, Program program) { + //throws an IllegalArgumentException if min,max don't defined a proper AddressRange + AddressSet range = new AddressSet(min, max); + AddressSetView loadedAndInit = program.getMemory().getLoadedAndInitializedAddressSet(); + if (!range.intersects(loadedAndInit)) { + return true; //range has no values + } + if (!loadedAndInit.contains(range)) { + return false; //range contains some address with values and some without + //this might be impossible + } + try { + Byte firstByte = program.getMemory().getByte(min); + for (Address addr = min.add(1); addr.compareTo(max) <= 0; addr = addr.add(1)) { + Byte val = program.getMemory().getByte(addr); + + if (!Objects.equals(val, firstByte)) { + return false; + } + } + } + catch (MemoryAccessException e) { + return false; //shouldn't happen + } + return true; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/AddressRangeTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/AddressRangeTableModel.java new file mode 100644 index 0000000000..2f6436fcd1 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/AddressRangeTableModel.java @@ -0,0 +1,254 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.codebrowser; + +import java.util.stream.StreamSupport; + +import docking.widgets.table.AbstractDynamicTableColumn; +import docking.widgets.table.TableColumnDescriptor; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.program.model.address.*; +import ghidra.program.model.listing.Program; +import ghidra.program.model.symbol.ReferenceManager; +import ghidra.program.util.ProgramLocation; +import ghidra.program.util.ProgramSelection; +import ghidra.util.Msg; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.exception.CancelledException; +import ghidra.util.table.GhidraProgramTableModel; +import ghidra.util.task.TaskMonitor; + +/** + * A {@link GhidraProgramTableModel} for displaying tables in which one row corresponds + * to an {@link AddressRange} + */ +public class AddressRangeTableModel extends GhidraProgramTableModel { + + private ProgramSelection selection; + private static final int MAX_ADDRESS_COLUMN_INDEX = 1; + private int resultsLimit; + private long minLength; + + protected AddressRangeTableModel(PluginTool tool, Program program, ProgramSelection selection, + int resultsLimit, long minLength) { + super("Selected Ranges in " + program.getName(), tool, program, null); + this.selection = selection; + this.resultsLimit = resultsLimit; + this.minLength = minLength; + } + + @Override + public ProgramLocation getProgramLocation(int modelRow, int modelColumn) { + AddressRangeInfo rangeInfo = getRowObject(modelRow); + if (modelColumn == MAX_ADDRESS_COLUMN_INDEX) { + return new ProgramLocation(program, rangeInfo.max()); + } + return new ProgramLocation(program, rangeInfo.min()); + } + + @Override + public ProgramSelection getProgramSelection(int[] modelRows) { + AddressSet ranges = new AddressSet(); + for (AddressRangeInfo rangeInfo : getRowObjects(modelRows)) { + ranges.addRange(program, rangeInfo.min(), rangeInfo.max()); + } + return new ProgramSelection(program.getAddressFactory(), ranges); + } + + @Override + protected void doLoad(Accumulator accumulator, TaskMonitor monitor) + throws CancelledException { + AddressRangeIterator rangeIter = selection.getAddressRanges(); + ReferenceManager refManager = program.getReferenceManager(); + while (rangeIter.hasNext()) { + monitor.checkCancelled(); + AddressRange range = rangeIter.next(); + if (range.getLength() < minLength) { + continue; + } + boolean isSameByte = AddressRangeInfo.isSameByteValue(range.getMinAddress(), + range.getMaxAddress(), program); + + AddressSet rangeSet = new AddressSet(range); + + AddressIterator destAddrIter = + refManager.getReferenceDestinationIterator(rangeSet, true); + int numRefsTo = StreamSupport.stream(destAddrIter.spliterator(), false) + .map(addr -> refManager.getReferenceCountTo(addr)) + .reduce(0, Integer::sum); + + AddressIterator srcAddrIter = + refManager.getReferenceSourceIterator(rangeSet, true); + int numRefsFrom = StreamSupport.stream(srcAddrIter.spliterator(), false) + .map(addr -> refManager.getReferenceCountFrom(addr)) + .reduce(0, Integer::sum); + + AddressRangeInfo info = new AddressRangeInfo(range.getMinAddress(), + range.getMaxAddress(), range.getLength(), isSameByte, numRefsTo, numRefsFrom); + + accumulator.add(info); + if (accumulator.size() >= resultsLimit) { + Msg.showWarn(this, null, "Results Truncated", + "Results are limited to " + resultsLimit + " address ranges.\n" + + "This limit can be changed by the tool option \"" + + CodeBrowserSelectionPlugin.OPTION_CATEGORY_NAME + " -> " + + CodeBrowserSelectionPlugin.RANGES_LIMIT_OPTION_NAME + "\"."); + break; + } + } + if (accumulator.isEmpty()) { + Msg.showWarn(this, null, "No Ranges to Display", + "No ranges to display - consider adjusting \"" + + CodeBrowserSelectionPlugin.OPTION_CATEGORY_NAME + " -> " + + CodeBrowserSelectionPlugin.MIN_RANGE_SIZE_OPTION_NAME + "\"."); + } + } + + @Override + public void refresh() { + reload(); + } + + @Override + protected TableColumnDescriptor createTableColumnDescriptor() { + TableColumnDescriptor descriptor = new TableColumnDescriptor<>(); + descriptor.addVisibleColumn(new MinAddressTableColumn()); + descriptor.addVisibleColumn(new MaxAddressTableColumn()); + descriptor.addVisibleColumn(new LengthTableColumn()); + descriptor.addVisibleColumn(new IdenticalBytesTableColumn()); + descriptor.addVisibleColumn(new NumRefsToTableColumn()); + descriptor.addVisibleColumn(new NumRefsFromTableColumn()); + descriptor.addVisibleColumn(new BlockNameTableColumn()); + descriptor.addHiddenColumn(new AddressRangeBytesTableColumn()); + descriptor.addHiddenColumn(new AddressRangeCodeUnitTableColumn()); + + return descriptor; + } + + private class MinAddressTableColumn + extends AbstractDynamicTableColumn { + + @Override + public String getColumnName() { + return "Min Address"; + } + + @Override + public Address getValue(AddressRangeInfo rangeInfo, Settings settings, Object data, + ServiceProvider services) throws IllegalArgumentException { + return rangeInfo.min(); + } + } + + private class MaxAddressTableColumn + extends AbstractDynamicTableColumn { + + @Override + public String getColumnName() { + return "Max Address"; + } + + @Override + public Address getValue(AddressRangeInfo rangeInfo, Settings settings, Object data, + ServiceProvider services) throws IllegalArgumentException { + return rangeInfo.max(); + } + } + + private class LengthTableColumn + extends AbstractDynamicTableColumn { + + @Override + public String getColumnName() { + return "Length"; + } + + @Override + public Long getValue(AddressRangeInfo rangeInfo, Settings settings, Object data, + ServiceProvider services) throws IllegalArgumentException { + return rangeInfo.size(); + } + } + + private class IdenticalBytesTableColumn + extends AbstractDynamicTableColumn { + + @Override + public String getColumnName() { + return "Identical Bytes"; + } + + @Override + public String getColumnDescription() { + return "Do all bytes in the range have the same value"; + } + + @Override + public Boolean getValue(AddressRangeInfo rangeInfo, Settings settings, Object data, + ServiceProvider services) throws IllegalArgumentException { + return rangeInfo.isSameByte(); + } + } + + private class BlockNameTableColumn + extends AbstractDynamicTableColumn { + + @Override + public String getColumnName() { + return "Block Name"; + } + + @Override + public String getValue(AddressRangeInfo rangeInfo, Settings settings, Object data, + ServiceProvider services) throws IllegalArgumentException { + return program.getMemory().getBlock(rangeInfo.min()).getName(); + } + } + + private class NumRefsToTableColumn + extends AbstractDynamicTableColumn { + + @Override + public String getColumnName() { + return "To References"; + } + + @Override + public Integer getValue(AddressRangeInfo rangeInfo, Settings settings, Object data, + ServiceProvider services) throws IllegalArgumentException { + return rangeInfo.numRefsTo(); + } + } + + private class NumRefsFromTableColumn + extends AbstractDynamicTableColumn { + + @Override + public String getColumnName() { + return "From References"; + } + + @Override + public Integer getValue(AddressRangeInfo rangeInfo, Settings settings, Object data, + ServiceProvider services) throws IllegalArgumentException { + return rangeInfo.numRefsFrom(); + } + } + + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/CodeBrowserSelectionPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/CodeBrowserSelectionPlugin.java index f83d7a041c..50807a6300 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/CodeBrowserSelectionPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/CodeBrowserSelectionPlugin.java @@ -20,13 +20,14 @@ import javax.swing.Icon; import docking.action.builder.ActionBuilder; import docking.tool.ToolConstants; import generic.theme.GIcon; -import ghidra.GhidraOptions; import ghidra.app.CorePluginPackage; import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.plugin.core.codebrowser.SelectEndpointsAction.RangeEndpoint; import ghidra.app.plugin.core.table.TableComponentProvider; import ghidra.app.util.HelpTopics; import ghidra.app.util.SearchConstants; import ghidra.app.util.query.TableService; +import ghidra.framework.options.Options; import ghidra.framework.options.ToolOptions; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; @@ -57,10 +58,22 @@ public class CodeBrowserSelectionPlugin extends Plugin { private static final String SELECT_GROUP = "Select Group"; private static final String SELECTION_LIMIT_OPTION_NAME = "Table From Selection Limit"; + private static final int SELECTION_LIMIT_DEFAULT = 500; + static final String RANGES_LIMIT_OPTION_NAME = "Ranges From Selection Limit"; + static final int RANGES_LIMIT_DEFAULT = 500; + static final String MIN_RANGE_SIZE_OPTION_NAME = + "Minimum Length of Address Range in Range Table"; + static final long MIN_RANGE_SIZE_DEFAULT = 1; + + static final String CREATE_ADDRESS_RANGE_TABLE_ACTION_NAME = + "Create Table From Ranges"; + + static final String OPTION_CATEGORY_NAME = "Selection Tables"; public CodeBrowserSelectionPlugin(PluginTool tool) { super(tool); createActions(); + initializeOptions(); } private void createActions() { @@ -98,11 +111,57 @@ public class CodeBrowserSelectionPlugin extends Plugin { new ActionBuilder("Create Table From Selection", getName()) .menuPath(ToolConstants.MENU_SELECTION, "Create Table From Selection") .menuGroup("SelectUtils") - .helpLocation(new HelpLocation("CodeBrowserPlugin", "Selection_Table")) + .helpLocation(new HelpLocation(HelpTopics.CODE_BROWSER, "Selection_Tables")) .withContext(CodeViewerActionContext.class, true) .inWindow(ActionBuilder.When.CONTEXT_MATCHES) .onAction(c -> createTable((CodeViewerProvider) c.getComponentProvider())) .buildAndInstall(tool); + + new ActionBuilder(CREATE_ADDRESS_RANGE_TABLE_ACTION_NAME, getName()) + .menuPath(ToolConstants.MENU_SELECTION, CREATE_ADDRESS_RANGE_TABLE_ACTION_NAME) + .menuGroup("SelectUtils") + .helpLocation(new HelpLocation(HelpTopics.CODE_BROWSER, "Selection_Tables")) + .withContext(CodeViewerActionContext.class, true) + .inWindow(ActionBuilder.When.CONTEXT_MATCHES) + .onAction( + c -> createAddressRangeTable((CodeViewerProvider) c.getComponentProvider())) + .buildAndInstall(tool); + + } + + private void createAddressRangeTable(CodeViewerProvider componentProvider) { + TableService tableService = tool.getService(TableService.class); + if (tableService == null) { + Msg.showWarn(this, null, "No Table Service", "Please add the TableServicePlugin."); + return; + } + Program program = componentProvider.getProgram(); + ProgramSelection selection = componentProvider.getSelection(); + if (selection.isEmpty()) { + tool.setStatusInfo("Unable to create selected ranges table: no addresses in selection"); + return; + } + ToolOptions options = tool.getOptions(OPTION_CATEGORY_NAME); + int resultsLimit = options.getInt(RANGES_LIMIT_OPTION_NAME, RANGES_LIMIT_DEFAULT); + long minLength = options.getLong(MIN_RANGE_SIZE_OPTION_NAME, MIN_RANGE_SIZE_DEFAULT); + AddressRangeTableModel model = + new AddressRangeTableModel(tool, program, selection, resultsLimit, minLength); + Icon markerIcon = new GIcon("icon.plugin.codebrowser.cursor.marker"); + String title = "Selected Ranges in " + program.getName(); + TableComponentProvider tableProvider = + tableService.showTableWithMarkers(title, "Selected Ranges", model, + SearchConstants.SEARCH_HIGHLIGHT_COLOR, markerIcon, title, null); + tableProvider.installRemoveItemsAction(); + + SelectEndpointsAction selectMin = + new SelectEndpointsAction(this, program, model, RangeEndpoint.MIN); + selectMin.setHelpLocation(new HelpLocation(HelpTopics.CODE_BROWSER, "Range_Table_Actions")); + tableProvider.addLocalAction(selectMin); + + SelectEndpointsAction selectMax = + new SelectEndpointsAction(this, program, model, RangeEndpoint.MAX); + selectMax.setHelpLocation(new HelpLocation(HelpTopics.CODE_BROWSER, "Range_Table_Actions")); + tableProvider.addLocalAction(selectMax); } @@ -156,9 +215,8 @@ public class CodeBrowserSelectionPlugin extends Plugin { public void load(Accumulator
accumulator, TaskMonitor monitor) throws CancelledException { - ToolOptions options = tool.getOptions(ToolConstants.TOOL_OPTIONS); - int resultsLimit = options.getInt(GhidraOptions.OPTION_SEARCH_LIMIT, - SearchConstants.DEFAULT_SEARCH_LIMIT); + ToolOptions options = tool.getOptions(OPTION_CATEGORY_NAME); + int resultsLimit = options.getInt(SELECTION_LIMIT_OPTION_NAME, SELECTION_LIMIT_DEFAULT); long size = selection.getNumAddresses(); monitor.initialize(size); @@ -167,7 +225,8 @@ public class CodeBrowserSelectionPlugin extends Plugin { if (accumulator.size() >= resultsLimit) { Msg.showWarn(this, null, "Results Truncated", "Results are limited to " + resultsLimit + " code units.\n" + - "This limit can be changed by the tool option \"Tool -> " + + "This limit can be changed by the tool option \"" + + OPTION_CATEGORY_NAME + " -> " + SELECTION_LIMIT_OPTION_NAME + "\"."); break; @@ -179,4 +238,16 @@ public class CodeBrowserSelectionPlugin extends Plugin { } } } + + private void initializeOptions() { + Options opt = tool.getOptions(OPTION_CATEGORY_NAME); + opt.registerOption(SELECTION_LIMIT_OPTION_NAME, SELECTION_LIMIT_DEFAULT, null, + "Maximum number of entries in selection table"); + + opt.registerOption(RANGES_LIMIT_OPTION_NAME, RANGES_LIMIT_DEFAULT, null, + "Maximum number of entries in an address range table"); + + opt.registerOption(MIN_RANGE_SIZE_OPTION_NAME, MIN_RANGE_SIZE_DEFAULT, null, + "Minimum length of an address range in address range table"); + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/SelectEndpointsAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/SelectEndpointsAction.java new file mode 100644 index 0000000000..3e603ec00f --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/SelectEndpointsAction.java @@ -0,0 +1,138 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.codebrowser; + +import java.awt.Component; +import java.util.*; + +import javax.swing.ImageIcon; +import javax.swing.JTable; +import javax.swing.table.TableModel; + +import docking.ActionContext; +import docking.action.*; +import ghidra.app.events.ProgramSelectionPluginEvent; +import ghidra.framework.plugintool.Plugin; +import ghidra.program.model.address.AddressSet; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramSelection; +import resources.*; + + + +/** + * An action for creating a {@link ProgramSelection} from rows of an {@link AddressRangeTableModel} + * using either the min addresses or the max addresses + */ +public class SelectEndpointsAction extends DockingAction { + private Program program; + private AddressRangeTableModel model; + private Plugin plugin; + private RangeEndpoint endpoint; + + enum RangeEndpoint { + MIN, MAX + } + + /** + * Creates an action which selects the endpoint of a range based on {@code RangeEndpoint} + * @param plugin plugin + * @param program program + * @param model model + * @param endpoint left or right endpoint + */ + public SelectEndpointsAction(Plugin plugin, Program program, + AddressRangeTableModel model, RangeEndpoint endpoint) { + super("Select " + endpoint.name(), plugin.getName()); + this.program = program; + this.model = model; + this.plugin = plugin; + this.endpoint = endpoint; + init(); + } + + @Override + public boolean isAddToPopup(ActionContext context) { + return true; + } + + @Override + public boolean isEnabledForContext(ActionContext context) { + return !getSelectedRanges(context).isEmpty(); + } + + @Override + public void actionPerformed(ActionContext context) { + AddressSet selection = new AddressSet(); + for (AddressRangeInfo rangeInfo : model.getLastSelectedObjects()) { + if (endpoint.equals(RangeEndpoint.MIN)) { + selection.add(rangeInfo.min()); + } + else { + selection.add(rangeInfo.max()); + } + } + plugin.getTool() + .firePluginEvent(new ProgramSelectionPluginEvent(plugin.getName(), + new ProgramSelection(selection), program)); + } + + private void init() { + ImageIcon icon = null; + String menuText = null; + String description = null; + int height = Icons.MAKE_SELECTION_ICON.getIconHeight()/2; + int weight = Icons.MAKE_SELECTION_ICON.getIconWidth()/2; + MultiIconBuilder iconBuilder = new MultiIconBuilder(Icons.MAKE_SELECTION_ICON); + if (endpoint.equals(RangeEndpoint.MIN)) { + iconBuilder.addIcon(Icons.UP_ICON, weight, height, QUADRANT.UL); + icon = iconBuilder.build(); + menuText = "Select Min Endpoints"; + description = + "Makes a Program Selection from the minimum addresses in the selected rows"; + } + else { + iconBuilder.addIcon(Icons.DOWN_ICON, weight, height, QUADRANT.LL); + icon = iconBuilder.build(); + menuText = "Select Max Endpoints"; + description = + "Makes a Program Selection from the maximum addresses in the selected rows"; + } + setPopupMenuData(new MenuData(new String[] { menuText }, icon)); + setToolBarData(new ToolBarData(icon)); + setDescription(description); + } + + private List getSelectedRanges(ActionContext context) { + Component component = context.getSourceComponent(); + if (!(component instanceof JTable table)) { + return Collections.emptyList(); + } + + TableModel tableModel = table.getModel(); + if (model != tableModel) { + return Collections.emptyList(); + } + + List ranges = new ArrayList<>(); + for (int row : table.getSelectedRows()) { + ranges.add(model.getRowObject(row)); + } + + return ranges; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/util/table/field/AddressRangeEndpointSettingsDefinition.java b/Ghidra/Features/Base/src/main/java/ghidra/util/table/field/AddressRangeEndpointSettingsDefinition.java new file mode 100644 index 0000000000..cc2001e946 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/util/table/field/AddressRangeEndpointSettingsDefinition.java @@ -0,0 +1,122 @@ +/* ### + * 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.util.table.field; + +import ghidra.docking.settings.EnumSettingsDefinition; +import ghidra.docking.settings.Settings; +import ghidra.program.model.address.AddressRange; + +/** + * A class for selecting whether to use the min address or the max address of an + * {@link AddressRange} for address range table columns + */ +public class AddressRangeEndpointSettingsDefinition implements EnumSettingsDefinition { + + private static final String ADDRESS_RANGE_ENDPOINT = "Address Range Endpoint"; + private static final String ENDPOINT = "Endpoint"; + public static final String BEGIN = "Begin"; + public static final String END = "End"; + private static final String[] CHOICES = { BEGIN, END }; + public static final int BEGIN_CHOICE_INDEX = 0; + public static final int END_CHOICE_INDEX = 1; + private static final int DEFAULT = 0; + public static final AddressRangeEndpointSettingsDefinition DEF = + new AddressRangeEndpointSettingsDefinition(); + + private AddressRangeEndpointSettingsDefinition() { + //singleton class + } + + @Override + public boolean hasValue(Settings setting) { + return setting.getValue(ADDRESS_RANGE_ENDPOINT) != null; + } + + @Override + public String getValueString(Settings settings) { + return CHOICES[getChoice(settings)]; + } + + @Override + public String getName() { + return ENDPOINT; + } + + @Override + public String getStorageKey() { + return ADDRESS_RANGE_ENDPOINT; + } + + @Override + public String getDescription() { + return "Selects the base address"; + } + + @Override + public void clear(Settings settings) { + settings.clearSetting(ADDRESS_RANGE_ENDPOINT); + + } + + @Override + public void copySetting(Settings srcSettings, Settings destSettings) { + Long l = srcSettings.getLong(ADDRESS_RANGE_ENDPOINT); + if (l == null) { + destSettings.clearSetting(ADDRESS_RANGE_ENDPOINT); + } + else { + destSettings.setLong(ADDRESS_RANGE_ENDPOINT, l); + } + + } + + @Override + public int getChoice(Settings settings) { + if (settings == null) { + return DEFAULT; + } + Long value = settings.getLong(ADDRESS_RANGE_ENDPOINT); + if (value == null || value < 0 || value >= CHOICES.length) { + return DEFAULT; + } + return value.intValue(); + } + + @Override + public void setChoice(Settings settings, int value) { + if (value < DEFAULT) { + settings.clearSetting(ADDRESS_RANGE_ENDPOINT); + } + else { + if (value > CHOICES.length) { + value = CHOICES.length; + } + settings.setLong(ADDRESS_RANGE_ENDPOINT, value); + } + + } + + @Override + public String getDisplayChoice(int value, Settings settings) { + return CHOICES[value]; + } + + @Override + public String[] getDisplayChoices(Settings settings) { + return CHOICES; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/util/table/field/BytesTableColumn.java b/Ghidra/Features/Base/src/main/java/ghidra/util/table/field/BytesTableColumn.java index e2704f8e1e..c815273328 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/util/table/field/BytesTableColumn.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/util/table/field/BytesTableColumn.java @@ -15,13 +15,6 @@ */ package ghidra.util.table.field; -import java.awt.Component; - -import javax.swing.JLabel; -import javax.swing.JTable; -import javax.swing.table.TableModel; - -import docking.widgets.table.GTableCellRenderingData; import ghidra.docking.settings.*; import ghidra.framework.plugintool.ServiceProvider; import ghidra.program.model.address.*; @@ -30,8 +23,6 @@ import ghidra.program.model.listing.*; import ghidra.program.model.mem.MemoryAccessException; import ghidra.program.util.BytesFieldLocation; import ghidra.program.util.ProgramLocation; -import ghidra.util.StringFormat; -import ghidra.util.table.column.AbstractGColumnRenderer; import ghidra.util.table.column.GColumnRenderer; /** @@ -49,98 +40,7 @@ public class BytesTableColumn extends ProgramLocationTableColumnExtensionPoint monospacedRenderer = new AbstractGColumnRenderer<>() { - @Override - protected void configureFont(JTable table, TableModel model, int column) { - setFont(getFixedWidthFont()); - } - - private String formatBytes(Byte[] bytes, Settings settings) { - boolean bigEndian = (ENDIANESS.getChoice(settings) != EndianSettingsDefinition.LITTLE); - - int startIx = 0; - int endIx = bytes.length; - int inc = 1; - if (!bigEndian) { - startIx = bytes.length - 1; - endIx = -1; - inc = -1; - } - - int format = FORMAT.getChoice(settings); - if (format == FormatSettingsDefinition.CHAR) { - return bytesToString(bytes); - } - - StringBuilder buffer = new StringBuilder(); - for (int i = startIx; i != endIx; i += inc) { - if (buffer.length() != 0) { - buffer.append(' '); - } - buffer.append(getByteString(bytes[i], format)); - } - return buffer.toString(); - } - - private String bytesToString(Byte[] bytes) { - StringBuilder buf = new StringBuilder(); - for (byte b : bytes) { - char c = (char) (b & 0xff); - if (c > 32 && c < 128) { - buf.append((char) (b & 0xff)); - } - else { - buf.append('.'); - } - } - return buf.toString(); - } - - private String getByteString(Byte b, int format) { - - String val; - switch (format) { - case FormatSettingsDefinition.DECIMAL: - val = Integer.toString(b); - break; - case FormatSettingsDefinition.BINARY: - val = Integer.toBinaryString(b & 0x0ff); - val = StringFormat.padIt(val, 8, (char) 0, true); - break; - case FormatSettingsDefinition.OCTAL: - val = Integer.toOctalString(b & 0x0ff); - val = StringFormat.padIt(val, 3, (char) 0, true); - break; - default: - case FormatSettingsDefinition.HEX: - val = Integer.toHexString(b & 0x0ff).toUpperCase(); - val = StringFormat.padIt(val, 2, (char) 0, true); - break; - } - return val; - } - - @Override - public Component getTableCellRendererComponent(GTableCellRenderingData data) { - - JLabel label = (JLabel) super.getTableCellRendererComponent(data); - - Object value = data.getValue(); - Settings settings = data.getColumnSettings(); - - Byte[] bytes = (Byte[]) value; - - setText(formatBytes(bytes, settings)); - - return label; - } - - @Override - public String getFilterString(Byte[] t, Settings settings) { - String formatted = formatBytes(t, settings); - return formatted; - } - }; + private final GColumnRenderer monospacedRenderer = new MonospacedByteRenderer(); /** * Default Constructor diff --git a/Ghidra/Features/Base/src/main/java/ghidra/util/table/field/MonospacedByteRenderer.java b/Ghidra/Features/Base/src/main/java/ghidra/util/table/field/MonospacedByteRenderer.java new file mode 100644 index 0000000000..92299dd144 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/util/table/field/MonospacedByteRenderer.java @@ -0,0 +1,123 @@ +/* ### + * 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.util.table.field; + +import java.awt.Component; + +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.table.TableModel; + +import docking.widgets.table.GTableCellRenderingData; +import ghidra.docking.settings.FormatSettingsDefinition; +import ghidra.docking.settings.Settings; +import ghidra.program.model.data.EndianSettingsDefinition; +import ghidra.util.StringFormat; +import ghidra.util.table.column.AbstractGColumnRenderer; + +public class MonospacedByteRenderer extends AbstractGColumnRenderer { + @Override + protected void configureFont(JTable table, TableModel model, int column) { + setFont(getFixedWidthFont()); + } + + private String formatBytes(Byte[] bytes, Settings settings) { + boolean bigEndian = (EndianSettingsDefinition.DEF + .getChoice(settings) != EndianSettingsDefinition.LITTLE); + + int startIx = 0; + int endIx = bytes.length; + int inc = 1; + if (!bigEndian) { + startIx = bytes.length - 1; + endIx = -1; + inc = -1; + } + + int format = FormatSettingsDefinition.DEF.getChoice(settings); + if (format == FormatSettingsDefinition.CHAR) { + return bytesToString(bytes); + } + + StringBuilder buffer = new StringBuilder(); + for (int i = startIx; i != endIx; i += inc) { + if (buffer.length() != 0) { + buffer.append(' '); + } + buffer.append(getByteString(bytes[i], format)); + } + return buffer.toString(); + } + + private String bytesToString(Byte[] bytes) { + StringBuilder buf = new StringBuilder(); + for (byte b : bytes) { + char c = (char) (b & 0xff); + if (c > 32 && c < 128) { + buf.append((char) (b & 0xff)); + } + else { + buf.append('.'); + } + } + return buf.toString(); + } + + private String getByteString(Byte b, int format) { + + String val; + switch (format) { + case FormatSettingsDefinition.DECIMAL: + val = Integer.toString(b); + break; + case FormatSettingsDefinition.BINARY: + val = Integer.toBinaryString(b & 0x0ff); + val = StringFormat.padIt(val, 8, (char) 0, true); + break; + case FormatSettingsDefinition.OCTAL: + val = Integer.toOctalString(b & 0x0ff); + val = StringFormat.padIt(val, 3, (char) 0, true); + break; + default: + case FormatSettingsDefinition.HEX: + val = Integer.toHexString(b & 0x0ff).toUpperCase(); + val = StringFormat.padIt(val, 2, (char) 0, true); + break; + } + return val; + } + + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + + JLabel label = (JLabel) super.getTableCellRendererComponent(data); + + Object value = data.getValue(); + Settings settings = data.getColumnSettings(); + + Byte[] bytes = (Byte[]) value; + + setText(formatBytes(bytes, settings)); + + return label; + } + + @Override + public String getFilterString(Byte[] t, Settings settings) { + String formatted = formatBytes(t, settings); + return formatted; + } +} diff --git a/Ghidra/Features/Base/src/main/resources/ghidra/app/plugin/core/totd/tips.txt b/Ghidra/Features/Base/src/main/resources/ghidra/app/plugin/core/totd/tips.txt index 70e5bac513..94b4b70295 100644 --- a/Ghidra/Features/Base/src/main/resources/ghidra/app/plugin/core/totd/tips.txt +++ b/Ghidra/Features/Base/src/main/resources/ghidra/app/plugin/core/totd/tips.txt @@ -102,4 +102,6 @@ You can quickly change the font size of the Listing, Decompiler or the Bytes win You can quickly control the font size from the Theme Editor dialog via Edit->Theme->Configure. +You can create a table whose rows correspond to the address ranges in a selection via Select->Create Table From Ranges. + This is the last tip. You can turn them off now. diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/codebrowser/CodeBrowserTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/codebrowser/CodeBrowserTest.java index 586169f31e..4c6abdef45 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/codebrowser/CodeBrowserTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/codebrowser/CodeBrowserTest.java @@ -35,6 +35,7 @@ import docking.widgets.table.GTable; import generic.test.TestUtils; import ghidra.app.cmd.data.CreateDataCmd; import ghidra.app.events.ProgramSelectionPluginEvent; +import ghidra.app.plugin.core.codebrowser.SelectEndpointsAction.RangeEndpoint; import ghidra.app.plugin.core.navigation.NextPrevAddressPlugin; import ghidra.app.plugin.core.symtable.SymbolTablePlugin; import ghidra.app.plugin.core.table.TableComponentProvider; @@ -49,7 +50,9 @@ import ghidra.program.util.InteriorSelection; import ghidra.program.util.ProgramSelection; import ghidra.test.AbstractGhidraHeadedIntegrationTest; import ghidra.test.TestEnv; +import ghidra.util.exception.CancelledException; import ghidra.util.table.AddressPreviewTableModel; +import ghidra.util.task.TaskMonitor; public class CodeBrowserTest extends AbstractGhidraHeadedIntegrationTest { private TestEnv env; @@ -168,7 +171,7 @@ public class CodeBrowserTest extends AbstractGhidraHeadedIntegrationTest { ListSelectionModel selectionModel = table.getSelectionModel(); // select all the rows - selectionModel.setSelectionInterval(0, table.getRowCount() - 1); + runSwing(() -> selectionModel.setSelectionInterval(0, table.getRowCount() - 1)); AddressPreviewTableModel addressModel = (AddressPreviewTableModel) tableProvider.getModel(); // get a selection from the model and make sure it matches the original selection @@ -182,6 +185,54 @@ public class CodeBrowserTest extends AbstractGhidraHeadedIntegrationTest { } } + @Test + public void testAddressRangeTableFromSelection() throws CancelledException { + AddressSet undefined = new AddressSet(program.getListing() + .getUndefinedRanges(program.getMemory().getAllInitializedAddressSet(), true, + TaskMonitor.DUMMY)); + setSelection(undefined); + DockingActionIf createTableAction = + getAction(tool, CodeBrowserSelectionPlugin.CREATE_ADDRESS_RANGE_TABLE_ACTION_NAME); + performAction(createTableAction, true); + TableComponentProvider tableProvider = + waitForComponentProvider(tool.getToolFrame(), TableComponentProvider.class, 2000); + Object threadedPanel = TestUtils.getInstanceField("threadedPanel", tableProvider); + GTable table = (GTable) TestUtils.getInstanceField("table", threadedPanel); + assertEquals(7, table.getRowCount()); + ListSelectionModel selectionModel = table.getSelectionModel(); + // select all the rows + runSwing(() -> selectionModel.setSelectionInterval(0, table.getRowCount() - 1)); + AddressRangeTableModel addressModel = (AddressRangeTableModel) tableProvider.getModel(); + + // get a selection from the model and make sure it matches the original selection + ProgramSelection tableProgramSelection = + addressModel.getProgramSelection(table.getSelectedRows()); + + AddressSet fromTable = new AddressSet(); + tableProgramSelection.getAddressRanges().forEach(r -> fromTable.add(r)); + assertEquals(fromTable, undefined); + + DockingActionIf selectMinsAction = + getAction(tool, "Select " + RangeEndpoint.MIN.name()); + assertNotNull(selectMinsAction); + performAction(selectMinsAction, true); + ProgramSelection minSelect = getCurrentSelection(); + assertEquals(7, minSelect.getNumAddresses()); + for (AddressRange range : fromTable.getAddressRanges()) { + assertTrue(minSelect.contains(range.getMinAddress())); + } + + DockingActionIf selectMaxesAction = + getAction(tool, "Select " + RangeEndpoint.MAX.name()); + assertNotNull(selectMaxesAction); + performAction(selectMaxesAction, true); + ProgramSelection maxSelect = getCurrentSelection(); + assertEquals(7, maxSelect.getNumAddresses()); + for (AddressRange range : fromTable.getAddressRanges()) { + assertTrue(maxSelect.contains(range.getMaxAddress())); + } + } + @Test public void testSimpleDragToCreateSelection() { ProgramSelection ps = dragToMakeSelection();