From 2a4b4f9bcfab54aa2b5474918da5c496fca2d75c Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Wed, 15 Jun 2022 15:41:38 -0400 Subject: [PATCH] GP-1969: Add 'Model' provider for inspecting object-based traces. --- .../DebuggerRegistersPlugin.html | 2 +- .../core/debug/gui/DebuggerResources.java | 156 +++- .../debug/gui/MultiProviderSaveBehavior.java | 115 +++ .../gui/model/AbstractQueryTableModel.java | 309 +++++++ .../gui/model/AbstractQueryTablePanel.java | 176 ++++ .../core/debug/gui/model/ColorsModified.java | 66 ++ .../debug/gui/model/DebuggerModelPlugin.java | 157 ++++ .../gui/model/DebuggerModelProvider.java | 681 +++++++++++++++ .../model/DebuggerObjectActionContext.java | 38 + .../debug/gui/model/DisplaysModified.java | 151 ++++ .../debug/gui/model/DisplaysObjectValues.java | 131 +++ .../core/debug/gui/model/ModelQuery.java | 165 ++++ .../debug/gui/model/ObjectTableModel.java | 412 ++++++++++ .../core/debug/gui/model/ObjectTreeModel.java | 777 ++++++++++++++++++ .../debug/gui/model/ObjectsTablePanel.java | 30 + .../debug/gui/model/ObjectsTreePanel.java | 265 ++++++ .../core/debug/gui/model/PathTableModel.java | 155 ++++ .../core/debug/gui/model/PathsTablePanel.java | 30 + .../model/columns/TracePathLastKeyColumn.java | 42 + .../columns/TracePathLastLifespanColumn.java | 44 + .../TracePathLastLifespanPlotColumn.java | 60 ++ .../model/columns/TracePathStringColumn.java | 36 + .../model/columns/TracePathValueColumn.java | 93 +++ .../model/columns/TraceValueKeyColumn.java | 35 + .../model/columns/TraceValueLifeColumn.java | 39 + .../columns/TraceValueLifePlotColumn.java | 56 ++ .../TraceValueObjectAttributeColumn.java | 180 ++++ .../model/columns/TraceValueValColumn.java | 116 +++ .../register/DebuggerRegistersProvider.java | 16 +- .../record/ObjectBasedTraceRecorder.java | 3 + .../service/model/record/ObjectRecorder.java | 22 + .../gui/model/DebuggerModelProviderTest.java | 766 +++++++++++++++++ .../core/debug/gui/model/ModelQueryTest.java | 61 ++ .../dbg/target/schema/TargetObjectSchema.java | 3 +- .../java/ghidra/dbg/util/AllPathsMatcher.java | 81 -- .../java/ghidra/dbg/util/PathMatcher.java | 55 +- .../java/ghidra/dbg/util/PathPattern.java | 75 +- .../java/ghidra/dbg/util/PathPredicates.java | 35 +- .../ghidra/dbg/util/PathPredicatesTest.java | 41 + .../DBTraceObjectBreakpointLocation.java | 2 +- .../trace/database/target/DBTraceObject.java | 48 +- .../DBTraceObjectAddressRangeValue.java | 19 + .../database/target/DBTraceObjectManager.java | 2 +- .../database/target/DBTraceObjectValue.java | 19 +- .../InternalAncestorsRelativeVisitor.java | 66 ++ ...java => InternalAncestorsRootVisitor.java} | 4 +- ...=> InternalSuccessorsRelativeVisitor.java} | 4 +- .../model/TraceDomainObjectListener.java | 8 + .../trace/model/target/TraceObject.java | 51 +- .../model/target/TraceObjectKeyPath.java | 21 +- .../trace/model/target/TraceObjectValue.java | 41 + .../table/RangeCursorTableHeaderRenderer.java | 32 +- .../table/RangeSetTableCellRenderer.java | 84 ++ .../widgets/table/RangeTableCellRenderer.java | 56 +- .../docking/widgets/table/RangedRenderer.java | 75 ++ .../table/AbstractDynamicTableColumn.java | 20 +- .../widgets/table/DynamicTableColumn.java | 67 +- .../table/GDynamicColumnTableModel.java | 91 +- .../widgets/table/MappedTableColumn.java | 15 +- 59 files changed, 6131 insertions(+), 269 deletions(-) create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/MultiProviderSaveBehavior.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractQueryTableModel.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractQueryTablePanel.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ColorsModified.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelPlugin.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProvider.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerObjectActionContext.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DisplaysModified.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DisplaysObjectValues.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ModelQuery.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTableModel.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTreeModel.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTablePanel.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTreePanel.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/PathTableModel.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/PathsTablePanel.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastKeyColumn.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastLifespanColumn.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastLifespanPlotColumn.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathStringColumn.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathValueColumn.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueKeyColumn.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueLifeColumn.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueLifePlotColumn.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueObjectAttributeColumn.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueValColumn.java create mode 100644 Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProviderTest.java create mode 100644 Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/ModelQueryTest.java delete mode 100644 Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/AllPathsMatcher.java create mode 100644 Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/PathPredicatesTest.java create mode 100644 Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalAncestorsRelativeVisitor.java rename Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/{InternalAncestorsVisitor.java => InternalAncestorsRootVisitor.java} (93%) rename Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/{InternalSuccessorsVisitor.java => InternalSuccessorsRelativeVisitor.java} (94%) create mode 100644 Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeSetTableCellRenderer.java create mode 100644 Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangedRenderer.java diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html index 48f4d9ab59..0cc6e5708e 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html @@ -100,7 +100,7 @@ and Service. Note: Only the raw "Value" column can be edited directly. The "Repr" column cannot be edited, yet.

-

Snapshot Window

+

Clone Window

This button is analogous to the "snapshot" action of other Ghidra windows. It generates a clone of this window. The clone will no longer follow the current thread, but it will follow diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java index 83528ff78f..1aaf6e54f1 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java @@ -39,6 +39,7 @@ import ghidra.app.plugin.core.debug.gui.console.DebuggerConsolePlugin; import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin; import ghidra.app.plugin.core.debug.gui.memory.DebuggerMemoryBytesPlugin; import ghidra.app.plugin.core.debug.gui.memory.DebuggerRegionsPlugin; +import ghidra.app.plugin.core.debug.gui.model.DebuggerModelPlugin; import ghidra.app.plugin.core.debug.gui.modules.DebuggerModulesPlugin; import ghidra.app.plugin.core.debug.gui.modules.DebuggerStaticMappingPlugin; import ghidra.app.plugin.core.debug.gui.objects.DebuggerObjectsPlugin; @@ -132,6 +133,9 @@ public interface DebuggerResources { ImageIcon ICON_SELECT_ROWS = ResourceManager.loadImage("images/table_go.png"); ImageIcon ICON_AUTOREAD = ResourceManager.loadImage("images/autoread.png"); + ImageIcon ICON_OBJECT_POPULATED = ResourceManager.loadImage("images/object-populated.png"); + ImageIcon ICON_OBJECT_UNPOPULATED = ResourceManager.loadImage("images/object-unpopulated.png"); + // TODO: Draw a real icon. ImageIcon ICON_REFRESH_MEMORY = ICON_REFRESH; @@ -255,6 +259,11 @@ public interface DebuggerResources { HelpLocation HELP_PROVIDER_OBJECTS = new HelpLocation( PluginUtils.getPluginNameFromClass(DebuggerObjectsPlugin.class), HELP_ANCHOR_PLUGIN); + String TITLE_PROVIDER_MODEL = "Model"; // TODO: An icon + ImageIcon ICON_PROVIDER_MODEL = ResourceManager.loadImage("images/function_graph.png"); + HelpLocation HELP_PROVIDER_MODEL = new HelpLocation( + PluginUtils.getPluginNameFromClass(DebuggerModelPlugin.class), HELP_ANCHOR_PLUGIN); + String TITLE_PROVIDER_WATCHES = "Watches"; ImageIcon ICON_PROVIDER_WATCHES = ICON_AUTOREAD; // TODO: Another icon? HelpLocation HELP_PROVIDER_WATCHES = new HelpLocation( @@ -275,6 +284,9 @@ public interface DebuggerResources { Color DEFAULT_COLOR_REGISTER_MARKERS = new Color(0.75f, 0.875f, 0.75f); ImageIcon ICON_REGISTER_MARKER = ResourceManager.loadImage("images/register-marker.png"); + ImageIcon ICON_EVENT_MARKER = ICON_REGISTER_MARKER; // TODO: Another icon? + // At least rename to "marker-arrow", and then have both ref it. + String OPTION_NAME_COLORS_REGISTER_STALE = "Colors.Stale Registers"; Color DEFAULT_COLOR_REGISTER_STALE = Color.GRAY; String OPTION_NAME_COLORS_REGISTER_STALE_SEL = "Colors.Stale Registers (selected)"; @@ -293,6 +305,11 @@ public interface DebuggerResources { String OPTION_NAME_COLORS_WATCH_CHANGED_SEL = "Colors.Changed Watches (selected)"; Color DEFAULT_COLOR_WATCH_CHANGED_SEL = ColorUtils.blend(Color.RED, Color.WHITE, 0.5f); + String OPTION_NAME_COLORS_VALUE_CHANGED = "Colors.Changed Values"; + Color DEFAULT_COLOR_VALUE_CHANGED = Color.RED; + String OPTION_NAME_COLORS_VALUE_CHANGED_SEL = "Colors.Changed Values (selected)"; + Color DEFAULT_COLOR_VALUE_CHANGED_SEL = ColorUtils.blend(Color.RED, Color.WHITE, 0.5f); + String OPTION_NAME_COLORS_PCODE_COUNTER = "Colors.Pcode Counter"; Color DEFAULT_COLOR_PCODE_COUNTER = new Color(0.75f, 0.875f, 0.75f); @@ -994,12 +1011,12 @@ public interface DebuggerResources { } } - interface CreateSnapshotAction { - String NAME = "Create Snapshot"; - String DESCRIPTION = "Create a (disconnected) snapshot copy of this window"; + interface CloneWindowAction { + String NAME = "Clone Window"; + String DESCRIPTION = "Create a disconnected copy of this window"; String GROUP = "zzzz"; Icon ICON = ResourceManager.loadImage("images/camera-photo.png"); - String HELP_ANCHOR = "snapshot_window"; + String HELP_ANCHOR = "clone_window"; static ActionBuilder builder(Plugin owner) { String ownerName = owner.getName(); @@ -1624,14 +1641,31 @@ public interface DebuggerResources { } } + interface StepSnapForwardAction { + String NAME = "Step Trace Snap Forward"; + String DESCRIPTION = "Navigate the recording forward one snap"; + Icon ICON = ICON_SNAP_FORWARD; + String GROUP = GROUP_CONTROL; + String HELP_ANCHOR = "step_trace_snap_forward"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP, "4") + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + abstract class AbstractStepSnapForwardAction extends DockingAction { - public static final String NAME = "Step Trace Snap Forward"; - public static final Icon ICON = ICON_SNAP_FORWARD; - public static final String HELP_ANCHOR = "step_trace_snap_forward"; + public static final String NAME = StepSnapForwardAction.NAME; + public static final Icon ICON = StepSnapForwardAction.ICON; + public static final String HELP_ANCHOR = StepSnapForwardAction.HELP_ANCHOR; public AbstractStepSnapForwardAction(Plugin owner) { super(NAME, owner.getName()); - setDescription("Navigate the recording forward one snap"); + setDescription(StepSnapForwardAction.DESCRIPTION); setHelpLocation(new HelpLocation(owner.getName(), HELP_ANCHOR)); } } @@ -1677,14 +1711,31 @@ public interface DebuggerResources { } } + interface StepSnapBackwardAction { + String NAME = "Step Trace Snap Backward"; + String DESCRIPTION = "Navigate the recording backward one snap"; + Icon ICON = ICON_SNAP_BACKWARD; + String GROUP = GROUP_CONTROL; + String HELP_ANCHOR = "step_trace_snap_backward"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP, "1") + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + abstract class AbstractStepSnapBackwardAction extends DockingAction { - public static final String NAME = "Step Trace Snap Backward"; - public static final Icon ICON = ICON_SNAP_BACKWARD; - public static final String HELP_ANCHOR = "step_trace_snap_backward"; + public static final String NAME = StepSnapBackwardAction.NAME; + public static final Icon ICON = StepSnapBackwardAction.ICON;; + public static final String HELP_ANCHOR = StepSnapBackwardAction.HELP_ANCHOR; public AbstractStepSnapBackwardAction(Plugin owner) { super(NAME, owner.getName()); - setDescription("Navigate the recording backward one snap"); + setDescription(StepSnapBackwardAction.DESCRIPTION); setHelpLocation(new HelpLocation(owner.getName(), HELP_ANCHOR)); } } @@ -2107,6 +2158,87 @@ public interface DebuggerResources { } } + interface LimitToCurrentSnapAction { + String NAME = "Limit to Current Snap"; + String DESCRIPTION = "Choose whether displayed objects must be alive at the current snap"; + String GROUP = GROUP_GENERAL; + Icon ICON = ICON_TIME; // TODO + String HELP_ANCHOR = "limit_to_current_snap"; + + static ToggleActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ToggleActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarGroup(GROUP) + .toolBarIcon(ICON) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface ShowHiddenAction { + String NAME = "Show Hidden"; + String DESCRIPTION = "Choose whether to display hidden children"; + String GROUP = GROUP_GENERAL; + String HELP_ANCHOR = "show_hidden"; + + static ToggleActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ToggleActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .menuPath(NAME) + .menuGroup(GROUP) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface ShowPrimitivesInTreeAction { + String NAME = "Show Primitives in Tree"; + String DESCRIPTION = "Choose whether to display primitive values in the tree"; + String GROUP = GROUP_GENERAL; + String HELP_ANCHOR = "show_primitives"; + + static ToggleActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ToggleActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .menuPath(NAME) + .menuGroup(GROUP) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface ShowMethodsInTreeAction { + String NAME = "Show Methods in Tree"; + String DESCRIPTION = "Choose whether to display methods in the tree"; + String GROUP = GROUP_GENERAL; + String HELP_ANCHOR = "show_methods"; + + static ToggleActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ToggleActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .menuPath(NAME) + .menuGroup(GROUP) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface FollowLinkAction { + String NAME = "Follow Link"; + String DESCRIPTION = "Navigate to the link target"; + String GROUP = GROUP_GENERAL; + String HELP_ANCHOR = "follow_link"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .popupMenuPath(NAME) + .popupMenuGroup(GROUP) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + public abstract class AbstractDebuggerConnectionsNode extends GTreeNode { @Override public String getName() { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/MultiProviderSaveBehavior.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/MultiProviderSaveBehavior.java new file mode 100644 index 0000000000..2620522e58 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/MultiProviderSaveBehavior.java @@ -0,0 +1,115 @@ +/* ### + * 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.debug.gui; + +import java.util.List; +import java.util.function.BiConsumer; + +import org.jdom.Element; + +import ghidra.app.plugin.core.debug.gui.MultiProviderSaveBehavior.SaveableProvider; +import ghidra.framework.options.SaveState; + +public abstract class MultiProviderSaveBehavior

{ + private static final String KEY_CONNECTED_PROVIDER = "connectedProvider"; + private static final String KEY_DISCONNECTED_COUNT = "disconnectedCount"; + private static final String PREFIX_DISCONNECTED_PROVIDER = "disconnectedProvider"; + + public interface SaveableProvider { + void writeConfigState(SaveState saveState); + + void readConfigState(SaveState saveState); + + void writeDataState(SaveState saveState); + + void readDataState(SaveState saveState); + } + + protected abstract P getConnectedProvider(); + + protected abstract List

getDisconnectedProviders(); + + protected abstract P createDisconnectedProvider(); + + protected abstract void removeDisconnectedProvider(P p); + + protected void doWrite(SaveState saveState, BiConsumer writer) { + P cp = getConnectedProvider(); + SaveState cpState = new SaveState(); + writer.accept(cp, cpState); + saveState.putXmlElement(KEY_CONNECTED_PROVIDER, cpState.saveToXml()); + + List

disconnectedProviders = getDisconnectedProviders(); + List

disconnected; + synchronized (disconnectedProviders) { + disconnected = List.copyOf(disconnectedProviders); + } + saveState.putInt(KEY_DISCONNECTED_COUNT, disconnected.size()); + for (int i = 0; i < disconnected.size(); i++) { + P dp = disconnected.get(i); + String stateName = PREFIX_DISCONNECTED_PROVIDER + i; + SaveState dpState = new SaveState(); + writer.accept(dp, dpState); + saveState.putXmlElement(stateName, dpState.saveToXml()); + } + } + + protected void doRead(SaveState saveState, BiConsumer reader, + boolean matchCount) { + Element cpElement = saveState.getXmlElement(KEY_CONNECTED_PROVIDER); + if (cpElement != null) { + P cp = getConnectedProvider(); + SaveState cpState = new SaveState(cpElement); + reader.accept(cp, cpState); + } + + int disconnectedCount = saveState.getInt(KEY_DISCONNECTED_COUNT, 0); + List

disconnectedProviders = getDisconnectedProviders(); + while (matchCount && disconnectedProviders.size() < disconnectedCount) { + createDisconnectedProvider(); + } + while (matchCount && disconnectedProviders.size() > disconnectedCount) { + removeDisconnectedProvider(disconnectedProviders.get(disconnectedProviders.size() - 1)); + } + + int count = Math.min(disconnectedCount, disconnectedProviders.size()); + for (int i = 0; i < count; i++) { + String stateName = PREFIX_DISCONNECTED_PROVIDER + i; + Element dpElement = saveState.getXmlElement(stateName); + if (dpElement != null) { + P dp = disconnectedProviders.get(i); + SaveState dpState = new SaveState(dpElement); + reader.accept(dp, dpState); + } + } + } + + public void writeConfigState(SaveState saveState) { + doWrite(saveState, P::writeConfigState); + } + + public void readConfigState(SaveState saveState) { + doRead(saveState, P::readConfigState, true); + } + + public void writeDataState(SaveState saveState) { + doWrite(saveState, P::writeDataState); + } + + public void readDataState(SaveState saveState) { + doRead(saveState, P::readDataState, false); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractQueryTableModel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractQueryTableModel.java new file mode 100644 index 0000000000..80d873db9c --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractQueryTableModel.java @@ -0,0 +1,309 @@ +/* ### + * 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.debug.gui.model; + +import java.awt.Color; +import java.util.Objects; +import java.util.stream.Stream; + +import com.google.common.collect.Range; + +import docking.widgets.table.threaded.ThreadedTableModel; +import ghidra.framework.plugintool.Plugin; +import ghidra.trace.database.DBTraceUtils; +import ghidra.trace.model.Trace; +import ghidra.trace.model.Trace.TraceObjectChangeType; +import ghidra.trace.model.Trace.TraceSnapshotChangeType; +import ghidra.trace.model.TraceDomainObjectListener; +import ghidra.trace.model.target.TraceObjectValue; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +public abstract class AbstractQueryTableModel extends ThreadedTableModel + implements DisplaysModified { + + protected class ListenerForChanges extends TraceDomainObjectListener { + public ListenerForChanges() { + listenFor(TraceObjectChangeType.VALUE_CREATED, this::valueCreated); + listenFor(TraceObjectChangeType.VALUE_DELETED, this::valueDeleted); + listenFor(TraceObjectChangeType.VALUE_LIFESPAN_CHANGED, this::valueLifespanChanged); + + listenFor(TraceSnapshotChangeType.ADDED, this::maxSnapChanged); + listenFor(TraceSnapshotChangeType.DELETED, this::maxSnapChanged); + } + + protected void valueCreated(TraceObjectValue value) { + if (query != null && query.includes(span, value)) { + reload(); // Can I be more surgical? + } + } + + protected void valueDeleted(TraceObjectValue value) { + if (query != null && query.includes(span, value)) { + reload(); + } + } + + protected void valueLifespanChanged(TraceObjectValue value, Range oldSpan, + Range newSpan) { + if (query == null) { + return; + } + boolean inOld = DBTraceUtils.intersect(oldSpan, span); + boolean inNew = DBTraceUtils.intersect(newSpan, span); + boolean queryIncludes = query.includes(Range.all(), value); + if (queryIncludes) { + if (inOld != inNew) { + reload(); + } + else if (inOld || inNew) { + refresh(); + } + } + } + + protected void maxSnapChanged() { + AbstractQueryTableModel.this.maxSnapChanged(); + } + } + + protected class TableDisplaysObjectValues implements DisplaysObjectValues { + @Override + public long getSnap() { + return snap; + } + } + + protected class DiffTableDisplaysObjectValues implements DisplaysObjectValues { + @Override + public long getSnap() { + return diffSnap; + } + } + + private Trace trace; + private long snap; + private Trace diffTrace; + private long diffSnap; + private ModelQuery query; + private Range span = Range.all(); + private boolean showHidden; + + private final ListenerForChanges listenerForChanges = newListenerForChanges(); + protected final DisplaysObjectValues display = new TableDisplaysObjectValues(); + protected final DisplaysObjectValues diffDisplay = new DiffTableDisplaysObjectValues(); + + protected AbstractQueryTableModel(String name, Plugin plugin) { + super(name, plugin.getTool(), null, true); + } + + protected ListenerForChanges newListenerForChanges() { + return new ListenerForChanges(); + } + + protected void maxSnapChanged() { + // Extension point + } + + private void removeOldTraceListener() { + if (trace != null) { + trace.removeListener(listenerForChanges); + } + } + + private void addNewTraceListener() { + if (trace != null) { + trace.addListener(listenerForChanges); + } + } + + protected void traceChanged() { + reload(); + } + + public void setTrace(Trace trace) { + if (Objects.equals(this.trace, trace)) { + return; + } + removeOldTraceListener(); + this.trace = trace; + addNewTraceListener(); + + traceChanged(); + } + + @Override + public Trace getTrace() { + return trace; + } + + protected void snapChanged() { + refresh(); + } + + public void setSnap(long snap) { + if (this.snap == snap) { + return; + } + this.snap = snap; + + snapChanged(); + } + + @Override + public long getSnap() { + return snap; + } + + protected void diffTraceChanged() { + refresh(); + } + + /** + * Set alternative trace to colorize values that differ + * + *

+ * The same trace can be used, but with an alternative snap, if desired. See + * {@link #setDiffSnap(long)}. One common use is to compare with the previous snap of the same + * trace. Another common use is to compare with the previous navigation. + * + * @param diffTrace the alternative trace + */ + public void setDiffTrace(Trace diffTrace) { + if (this.diffTrace == diffTrace) { + return; + } + this.diffTrace = diffTrace; + diffTraceChanged(); + } + + @Override + public Trace getDiffTrace() { + return diffTrace; + } + + protected void diffSnapChanged() { + refresh(); + } + + /** + * Set alternative snap to colorize values that differ + * + *

+ * The diff trace must be set, even if it's the same as the trace being displayed. See + * {@link #setDiffTrace(Trace)}. + * + * @param diffSnap the alternative snap + */ + public void setDiffSnap(long diffSnap) { + if (this.diffSnap == diffSnap) { + return; + } + this.diffSnap = diffSnap; + diffSnapChanged(); + } + + @Override + public long getDiffSnap() { + return diffSnap; + } + + protected void queryChanged() { + reload(); + } + + public void setQuery(ModelQuery query) { + if (Objects.equals(this.query, query)) { + return; + } + this.query = query; + + queryChanged(); + } + + public ModelQuery getQuery() { + return query; + } + + protected void spanChanged() { + reload(); + } + + public void setSpan(Range span) { + if (Objects.equals(this.span, span)) { + return; + } + this.span = span; + + spanChanged(); + } + + public Range getSpan() { + return span; + } + + protected void showHiddenChanged() { + reload(); + } + + public void setShowHidden(boolean showHidden) { + if (this.showHidden == showHidden) { + return; + } + this.showHidden = showHidden; + + showHiddenChanged(); + } + + public boolean isShowHidden() { + return showHidden; + } + + protected abstract Stream streamRows(Trace trace, ModelQuery query, Range span); + + @Override + protected void doLoad(Accumulator accumulator, TaskMonitor monitor) + throws CancelledException { + if (trace == null || query == null || trace.getObjectManager().getRootSchema() == null) { + return; + } + for (T t : (Iterable) streamRows(trace, query, span)::iterator) { + accumulator.add(t); + monitor.checkCanceled(); + } + } + + @Override + public Trace getDataSource() { + return trace; + } + + @Override + public boolean isEdgesDiffer(TraceObjectValue newEdge, TraceObjectValue oldEdge) { + if (DisplaysModified.super.isEdgesDiffer(newEdge, oldEdge)) { + return true; + } + // Hack to incorporate _display logic to differencing. + // This ensures "boxed" primitives show as differing at the object level + return !Objects.equals(diffDisplay.getEdgeDisplay(oldEdge), + display.getEdgeDisplay(newEdge)); + } + + public abstract void setDiffColor(Color diffColor); + + public abstract void setDiffColorSel(Color diffColorSel); +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractQueryTablePanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractQueryTablePanel.java new file mode 100644 index 0000000000..b4bcb7d91b --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractQueryTablePanel.java @@ -0,0 +1,176 @@ +/* ### + * 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.debug.gui.model; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.event.KeyListener; +import java.awt.event.MouseListener; +import java.util.List; + +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.event.ListSelectionListener; + +import com.google.common.collect.Range; + +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.framework.plugintool.Plugin; +import ghidra.util.table.GhidraTable; +import ghidra.util.table.GhidraTableFilterPanel; + +public abstract class AbstractQueryTablePanel extends JPanel { + + protected final AbstractQueryTableModel tableModel; + protected final GhidraTable table; + private final GhidraTableFilterPanel filterPanel; + + protected DebuggerCoordinates current = DebuggerCoordinates.NOWHERE; + protected boolean limitToSnap = false; + protected boolean showHidden = false; + + public AbstractQueryTablePanel(Plugin plugin) { + super(new BorderLayout()); + tableModel = createModel(plugin); + table = new GhidraTable(tableModel); + filterPanel = new GhidraTableFilterPanel<>(table, tableModel); + + add(new JScrollPane(table), BorderLayout.CENTER); + add(filterPanel, BorderLayout.SOUTH); + } + + protected abstract AbstractQueryTableModel createModel(Plugin plugin); + + public void goToCoordinates(DebuggerCoordinates coords) { + if (DebuggerCoordinates.equalsIgnoreRecorderAndView(current, coords)) { + return; + } + DebuggerCoordinates previous = current; + this.current = coords; + tableModel.setDiffTrace(previous.getTrace()); + tableModel.setTrace(current.getTrace()); + tableModel.setDiffSnap(previous.getSnap()); + tableModel.setSnap(current.getSnap()); + if (limitToSnap) { + tableModel.setSpan(Range.singleton(current.getSnap())); + } + } + + public void reload() { + tableModel.reload(); + } + + public void setQuery(ModelQuery query) { + tableModel.setQuery(query); + } + + public ModelQuery getQuery() { + return tableModel.getQuery(); + } + + public void setLimitToSnap(boolean limitToSnap) { + if (this.limitToSnap == limitToSnap) { + return; + } + this.limitToSnap = limitToSnap; + tableModel.setSpan(limitToSnap ? Range.singleton(current.getSnap()) : Range.all()); + } + + public boolean isLimitToSnap() { + return limitToSnap; + } + + public void setShowHidden(boolean showHidden) { + if (this.showHidden == showHidden) { + return; + } + this.showHidden = showHidden; + tableModel.setShowHidden(showHidden); + } + + public boolean isShowHidden() { + return showHidden; + } + + public void addSelectionListener(ListSelectionListener listener) { + table.getSelectionModel().addListSelectionListener(listener); + } + + public void removeSelectionListener(ListSelectionListener listener) { + table.getSelectionModel().removeListSelectionListener(listener); + } + + @Override + public synchronized void addMouseListener(MouseListener l) { + super.addMouseListener(l); + // HACK? + table.addMouseListener(l); + } + + @Override + public synchronized void removeMouseListener(MouseListener l) { + super.removeMouseListener(l); + // HACK? + table.removeMouseListener(l); + } + + @Override + public synchronized void addKeyListener(KeyListener l) { + super.addKeyListener(l); + // HACK? + table.addKeyListener(l); + } + + @Override + public synchronized void removeKeyListener(KeyListener l) { + super.removeKeyListener(l); + // HACK? + table.removeKeyListener(l); + } + + public void setSelectionMode(int selectionMode) { + table.setSelectionMode(selectionMode); + } + + public int getSelectionMode() { + return table.getSelectionModel().getSelectionMode(); + } + + // TODO: setSelectedItems? Is a bit more work than expected: + // see filterPanel.getTableFilterModel(); + // see table.getSelectionMode().addSelectionInterval() + // seems like setSelectedItems should be in filterPanel? + + public void setSelectedItem(T item) { + filterPanel.setSelectedItem(item); + } + + public List getSelectedItems() { + return filterPanel.getSelectedItems(); + } + + public T getSelectedItem() { + return filterPanel.getSelectedItem(); + } + + public void setDiffColor(Color diffColor) { + tableModel.setDiffColor(diffColor); + } + + public void setDiffColorSel(Color diffColorSel) { + tableModel.setDiffColorSel(diffColorSel); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ColorsModified.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ColorsModified.java new file mode 100644 index 0000000000..afa961ab86 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ColorsModified.java @@ -0,0 +1,66 @@ +/* ### + * 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.debug.gui.model; + +import java.awt.Color; + +import javax.swing.*; +import javax.swing.tree.TreeCellRenderer; + +public interface ColorsModified

{ + + Color getDiffForeground(P p); + + Color getDiffSelForeground(P p); + + Color getForeground(P p); + + Color getSelForeground(P p); + + default Color getForegroundFor(P p, boolean isModified, boolean isSelected) { + return isModified ? isSelected ? getDiffSelForeground(p) : getDiffForeground(p) + : isSelected ? getSelForeground(p) : getForeground(p); + } + + interface InTable extends ColorsModified { + @Override + default Color getForeground(JTable table) { + return table.getForeground(); + } + + @Override + default Color getSelForeground(JTable table) { + return table.getSelectionForeground(); + } + } + + interface InTree extends ColorsModified, TreeCellRenderer { + + Color getTextNonSelectionColor(); + + Color getTextSelectionColor(); + + @Override + default Color getForeground(JTree tree) { + return getTextNonSelectionColor(); + } + + @Override + default Color getSelForeground(JTree tree) { + return getTextSelectionColor(); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelPlugin.java new file mode 100644 index 0000000000..bf65d7c6e0 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelPlugin.java @@ -0,0 +1,157 @@ +/* ### + * 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.debug.gui.model; + +import java.util.*; + +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.plugin.core.debug.DebuggerPluginPackage; +import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent; +import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent; +import ghidra.app.plugin.core.debug.gui.MultiProviderSaveBehavior; +import ghidra.app.services.DebuggerTraceManagerService; +import ghidra.framework.options.SaveState; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.trace.model.Trace; + +@PluginInfo( + shortDescription = "Debugger model browser", + description = "GUI to browse objects recorded to the trace", + category = PluginCategoryNames.DEBUGGER, + packageName = DebuggerPluginPackage.NAME, + status = PluginStatus.STABLE, + eventsConsumed = { + TraceActivatedPluginEvent.class, + TraceClosedPluginEvent.class, + }, + servicesRequired = { + DebuggerTraceManagerService.class, + }) +public class DebuggerModelPlugin extends Plugin { + + private final class ForModelMultiProviderSaveBehavior + extends MultiProviderSaveBehavior { + @Override + protected DebuggerModelProvider getConnectedProvider() { + return connectedProvider; + } + + @Override + protected List getDisconnectedProviders() { + return disconnectedProviders; + } + + @Override + protected DebuggerModelProvider createDisconnectedProvider() { + return DebuggerModelPlugin.this.createDisconnectedProvider(); + } + + @Override + protected void removeDisconnectedProvider(DebuggerModelProvider p) { + p.removeFromTool(); + } + } + + private DebuggerModelProvider connectedProvider; + private final List disconnectedProviders = new ArrayList<>(); + private final ForModelMultiProviderSaveBehavior saveBehavior = + new ForModelMultiProviderSaveBehavior(); + + public DebuggerModelPlugin(PluginTool tool) { + super(tool); + } + + @Override + protected void init() { + this.connectedProvider = newProvider(false); + super.init(); + } + + @Override + protected void dispose() { + tool.removeComponentProvider(connectedProvider); + super.dispose(); + } + + protected DebuggerModelProvider newProvider(boolean isClone) { + return new DebuggerModelProvider(this, isClone); + } + + protected DebuggerModelProvider createDisconnectedProvider() { + DebuggerModelProvider p = newProvider(true); + synchronized (disconnectedProviders) { + disconnectedProviders.add(p); + } + return p; + } + + public DebuggerModelProvider getConnectedProvider() { + return connectedProvider; + } + + public List getDisconnectedProviders() { + return Collections.unmodifiableList(disconnectedProviders); + } + + @Override + public void processEvent(PluginEvent event) { + super.processEvent(event); + if (event instanceof TraceActivatedPluginEvent) { + TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event; + connectedProvider.coordinatesActivated(ev.getActiveCoordinates()); + } + if (event instanceof TraceClosedPluginEvent) { + TraceClosedPluginEvent ev = (TraceClosedPluginEvent) event; + traceClosed(ev.getTrace()); + } + } + + private void traceClosed(Trace trace) { + connectedProvider.traceClosed(trace); + synchronized (disconnectedProviders) { + for (DebuggerModelProvider p : disconnectedProviders) { + p.traceClosed(trace); + } + } + } + + void providerRemoved(DebuggerModelProvider p) { + synchronized (disconnectedProviders) { + disconnectedProviders.remove(p); + } + } + + @Override + public void writeConfigState(SaveState saveState) { + saveBehavior.writeConfigState(saveState); + } + + @Override + public void readConfigState(SaveState saveState) { + saveBehavior.readConfigState(saveState); + } + + @Override + public void writeDataState(SaveState saveState) { + saveBehavior.writeDataState(saveState); + } + + @Override + public void readDataState(SaveState saveState) { + saveBehavior.readDataState(saveState); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProvider.java new file mode 100644 index 0000000000..85dab75099 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProvider.java @@ -0,0 +1,681 @@ +/* ### + * 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.debug.gui.model; + +import java.awt.*; +import java.awt.event.*; +import java.lang.invoke.MethodHandles; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.swing.*; + +import docking.*; +import docking.action.DockingAction; +import docking.action.ToggleDockingAction; +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.plugin.core.debug.DebuggerPluginPackage; +import ghidra.app.plugin.core.debug.gui.DebuggerResources; +import ghidra.app.plugin.core.debug.gui.DebuggerResources.*; +import ghidra.app.plugin.core.debug.gui.MultiProviderSaveBehavior.SaveableProvider; +import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ObjectRow; +import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow; +import ghidra.app.plugin.core.debug.gui.model.ObjectTreeModel.AbstractNode; +import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow; +import ghidra.app.services.DebuggerTraceManagerService; +import ghidra.framework.options.AutoOptions; +import ghidra.framework.options.SaveState; +import ghidra.framework.options.annotation.*; +import ghidra.framework.plugintool.AutoConfigState; +import ghidra.framework.plugintool.AutoService; +import ghidra.framework.plugintool.annotation.AutoConfigStateField; +import ghidra.framework.plugintool.annotation.AutoServiceConsumed; +import ghidra.trace.model.Trace; +import ghidra.trace.model.target.*; +import ghidra.util.Msg; + +public class DebuggerModelProvider extends ComponentProvider implements SaveableProvider { + + private static final AutoConfigState.ClassHandler CONFIG_STATE_HANDLER = + AutoConfigState.wireHandler(DebuggerModelProvider.class, MethodHandles.lookup()); + private static final String KEY_DEBUGGER_COORDINATES = "DebuggerCoordinates"; + private static final String KEY_PATH = "Path"; + + private final DebuggerModelPlugin plugin; + private final boolean isClone; + + private JPanel mainPanel = new JPanel(new BorderLayout()); + + protected JTextField pathField; + protected JButton goButton; + protected ObjectsTreePanel objectsTreePanel; + protected ObjectsTablePanel elementsTablePanel; + protected PathsTablePanel attributesTablePanel; + + /*testing*/ DebuggerCoordinates current = DebuggerCoordinates.NOWHERE; + /*testing*/ TraceObjectKeyPath path = TraceObjectKeyPath.of(); + + @AutoServiceConsumed + protected DebuggerTraceManagerService traceManager; + @SuppressWarnings("unused") + private final AutoService.Wiring autoServiceWiring; + + @AutoOptionDefined( + description = "Text color for values that have just changed", + name = DebuggerResources.OPTION_NAME_COLORS_VALUE_CHANGED, + help = @HelpInfo(anchor = "colors")) + private Color diffColor = DebuggerResources.DEFAULT_COLOR_VALUE_CHANGED; + + @AutoOptionDefined( + description = "Select text color for values that have just changed", + name = DebuggerResources.OPTION_NAME_COLORS_VALUE_CHANGED_SEL, + help = @HelpInfo(anchor = "colors")) + private Color diffColorSel = DebuggerResources.DEFAULT_COLOR_VALUE_CHANGED_SEL; + + @SuppressWarnings("unused") + private final AutoOptions.Wiring autoOptionsWiring; + + @AutoConfigStateField + private boolean limitToSnap = false; + @AutoConfigStateField + private boolean showHidden = false; + @AutoConfigStateField + private boolean showPrimitivesInTree = false; + @AutoConfigStateField + private boolean showMethodsInTree = false; + + DockingAction actionCloneWindow; + ToggleDockingAction actionLimitToCurrentSnap; + ToggleDockingAction actionShowHidden; + ToggleDockingAction actionShowPrimitivesInTree; + ToggleDockingAction actionShowMethodsInTree; + DockingAction actionFollowLink; + // TODO: Remove stopgap + DockingAction actionStepBackward; + DockingAction actionStepForward; + + DebuggerObjectActionContext myActionContext; + + public DebuggerModelProvider(DebuggerModelPlugin plugin, boolean isClone) { + super(plugin.getTool(), DebuggerResources.TITLE_PROVIDER_MODEL, plugin.getName()); + this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this); + this.autoOptionsWiring = AutoOptions.wireOptions(plugin, this); + this.plugin = plugin; + this.isClone = isClone; + + setIcon(DebuggerResources.ICON_PROVIDER_MODEL); + setHelpLocation(DebuggerResources.HELP_PROVIDER_MODEL); + setWindowMenuGroup(DebuggerPluginPackage.NAME); + + buildMainPanel(); + + setDefaultWindowPosition(WindowPosition.LEFT); + createActions(); + + if (isClone) { + setTitle("[" + DebuggerResources.TITLE_PROVIDER_MODEL + "]"); + setWindowGroup("Debugger.Core.disconnected"); + setIntraGroupPosition(WindowPosition.STACK); + mainPanel.setBorder(BorderFactory.createLineBorder(Color.ORANGE, 2)); + setTransient(); + } + else { + setTitle(DebuggerResources.TITLE_PROVIDER_MODEL); + setWindowGroup("Debugger.Core"); + } + + doSetLimitToCurrentSnap(limitToSnap); + + setVisible(true); + contextChanged(); + } + + @Override + public void removeFromTool() { + plugin.providerRemoved(this); + super.removeFromTool(); + } + + protected void buildMainPanel() { + pathField = new JTextField(); + pathField.setInputVerifier(new InputVerifier() { + @Override + public boolean verify(JComponent input) { + try { + setPath(TraceObjectKeyPath.parse(pathField.getText()), pathField); + return true; + } + catch (IllegalArgumentException e) { + plugin.getTool().setStatusInfo("Invalid Path: " + pathField.getText(), true); + return false; + } + } + }); + goButton = new JButton("Go"); + ActionListener gotoPath = evt -> { + try { + setPath(TraceObjectKeyPath.parse(pathField.getText()), pathField); + KeyboardFocusManager.getCurrentKeyboardFocusManager().clearGlobalFocusOwner(); + } + catch (IllegalArgumentException e) { + Msg.showError(this, mainPanel, DebuggerResources.TITLE_PROVIDER_MODEL, + "Invalid Query: " + pathField.getText()); + } + }; + goButton.addActionListener(gotoPath); + pathField.addActionListener(gotoPath); + pathField.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { + pathField.setText(path.toString()); + KeyboardFocusManager.getCurrentKeyboardFocusManager().clearGlobalFocusOwner(); + } + } + }); + + objectsTreePanel = new ObjectsTreePanel(); + elementsTablePanel = new ObjectsTablePanel(plugin); + attributesTablePanel = new PathsTablePanel(plugin); + + JSplitPane lrSplit = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); + lrSplit.setResizeWeight(0.2); + JSplitPane tbSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT); + tbSplit.setResizeWeight(0.7); + lrSplit.setRightComponent(tbSplit); + + JPanel queryPanel = new JPanel(new BorderLayout()); + + queryPanel.add(new JLabel("Path: "), BorderLayout.WEST); + queryPanel.add(pathField, BorderLayout.CENTER); + queryPanel.add(goButton, BorderLayout.EAST); + + JPanel labeledElementsTablePanel = new JPanel(new BorderLayout()); + labeledElementsTablePanel.add(elementsTablePanel); + labeledElementsTablePanel.add(new JLabel("Elements"), BorderLayout.NORTH); + + JPanel labeledAttributesTablePanel = new JPanel(new BorderLayout()); + labeledAttributesTablePanel.add(attributesTablePanel); + labeledAttributesTablePanel.add(new JLabel("Attributes"), BorderLayout.NORTH); + + lrSplit.setLeftComponent(objectsTreePanel); + tbSplit.setLeftComponent(labeledElementsTablePanel); + tbSplit.setRightComponent(labeledAttributesTablePanel); + + mainPanel.add(queryPanel, BorderLayout.NORTH); + mainPanel.add(lrSplit, BorderLayout.CENTER); + + objectsTreePanel.addTreeSelectionListener(evt -> { + Trace trace = current.getTrace(); + if (trace == null) { + return; + } + if (trace.getObjectManager().getRootObject() == null) { + return; + } + List sel = objectsTreePanel.getSelectedItems(); + if (!sel.isEmpty()) { + myActionContext = new DebuggerObjectActionContext(sel.stream() + .map(n -> n.getValue()) + .collect(Collectors.toList()), + this, objectsTreePanel); + } + else { + myActionContext = null; + } + contextChanged(); + + if (sel.size() != 1) { + // TODO: Multiple paths? PathMatcher can do it, just have to parse + // Just leave whatever was there. + return; + } + TraceObjectValue value = sel.get(0).getValue(); + TraceObject parent = value.getParent(); + TraceObjectKeyPath path; + if (parent == null) { + path = TraceObjectKeyPath.of(); + } + else { + path = parent.getCanonicalPath().key(value.getEntryKey()); + } + setPath(path, objectsTreePanel); + }); + elementsTablePanel.addSelectionListener(evt -> { + if (evt.getValueIsAdjusting()) { + return; + } + List sel = elementsTablePanel.getSelectedItems(); + if (!sel.isEmpty()) { + myActionContext = new DebuggerObjectActionContext(sel.stream() + .map(r -> r.getValue()) + .collect(Collectors.toList()), + this, elementsTablePanel); + } + else { + myActionContext = null; + } + contextChanged(); + + if (sel.size() != 1) { + attributesTablePanel.setQuery(ModelQuery.attributesOf(path)); + return; + } + TraceObjectValue value = sel.get(0).getValue(); + if (!value.isObject()) { + return; + } + attributesTablePanel + .setQuery(ModelQuery.attributesOf(value.getChild().getCanonicalPath())); + }); + attributesTablePanel.addSelectionListener(evt -> { + if (evt.getValueIsAdjusting()) { + return; + } + List sel = attributesTablePanel.getSelectedItems(); + if (!sel.isEmpty()) { + myActionContext = new DebuggerObjectActionContext(sel.stream() + .map(r -> Objects.requireNonNull(r.getPath().getLastEntry())) + .collect(Collectors.toList()), + this, attributesTablePanel); + } + else { + myActionContext = null; + } + contextChanged(); + }); + + elementsTablePanel.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() != 2 || e.getButton() != MouseEvent.BUTTON1) { + return; + } + activatedElementsTable(); + } + }); + elementsTablePanel.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() != KeyEvent.VK_ENTER) { + return; + } + activatedElementsTable(); + e.consume(); + } + }); + attributesTablePanel.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() != 2 || e.getButton() != MouseEvent.BUTTON1) { + return; + } + activatedAttributesTable(); + } + }); + attributesTablePanel.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() != KeyEvent.VK_ENTER) { + return; + } + activatedAttributesTable(); + e.consume(); + } + }); + } + + @Override + public ActionContext getActionContext(MouseEvent event) { + if (myActionContext != null) { + return myActionContext; + } + return super.getActionContext(event); + } + + protected void createActions() { + actionCloneWindow = CloneWindowAction.builder(plugin) + .enabledWhen(c -> current.getTrace() != null) + .onAction(c -> activatedCloneWindow()) + .buildAndInstallLocal(this); + actionLimitToCurrentSnap = LimitToCurrentSnapAction.builder(plugin) + .onAction(this::toggledLimitToCurrentSnap) + .buildAndInstallLocal(this); + actionShowHidden = ShowHiddenAction.builder(plugin) + .onAction(this::toggledShowHidden) + .buildAndInstallLocal(this); + actionShowPrimitivesInTree = ShowPrimitivesInTreeAction.builder(plugin) + .onAction(this::toggledShowPrimitivesInTree) + .buildAndInstallLocal(this); + actionShowMethodsInTree = ShowMethodsInTreeAction.builder(plugin) + .onAction(this::toggledShowMethodsInTree) + .buildAndInstallLocal(this); + actionFollowLink = FollowLinkAction.builder(plugin) + .withContext(DebuggerObjectActionContext.class) + .enabledWhen(this::hasSingleLink) + .onAction(this::activatedFollowLink) + .buildAndInstallLocal(this); + + // TODO: These are a stopgap until the plot column header provides nav + actionStepBackward = StepSnapBackwardAction.builder(plugin) + .enabledWhen(this::isStepBackwardEnabled) + .onAction(this::activatedStepBackward) + .buildAndInstallLocal(this); + actionStepForward = StepSnapForwardAction.builder(plugin) + .enabledWhen(this::isStepForwardEnabled) + .onAction(this::activatedStepForward) + .buildAndInstallLocal(this); + } + + private void activatedElementsTable() { + ValueRow row = elementsTablePanel.getSelectedItem(); + if (row == null) { + return; + } + if (!(row instanceof ObjectRow)) { + return; + } + ObjectRow objectRow = (ObjectRow) row; + setPath(objectRow.getTraceObject().getCanonicalPath()); + } + + private void activatedAttributesTable() { + PathRow row = attributesTablePanel.getSelectedItem(); + if (row == null) { + return; + } + Object value = row.getValue(); + if (!(value instanceof TraceObject)) { + return; + } + TraceObject object = (TraceObject) value; + setPath(object.getCanonicalPath()); + } + + private void activatedCloneWindow() { + DebuggerModelProvider clone = plugin.createDisconnectedProvider(); + SaveState configState = new SaveState(); + this.writeConfigState(configState); + clone.readConfigState(configState); + SaveState dataState = new SaveState(); + this.writeDataState(dataState); + // coords are omitted by main window + // also, cannot save unless trace is in a project + clone.coordinatesActivated(current); + clone.readDataState(dataState); + plugin.getTool().showComponentProvider(clone, true); + } + + private void toggledLimitToCurrentSnap(ActionContext ctx) { + setLimitToCurrentSnap(actionLimitToCurrentSnap.isSelected()); + } + + private void toggledShowHidden(ActionContext ctx) { + setShowHidden(actionShowHidden.isSelected()); + } + + private void toggledShowPrimitivesInTree(ActionContext ctx) { + setShowPrimitivesInTree(actionShowPrimitivesInTree.isSelected()); + } + + private void toggledShowMethodsInTree(ActionContext ctx) { + setShowMethodsInTree(actionShowMethodsInTree.isSelected()); + } + + private boolean hasSingleLink(DebuggerObjectActionContext ctx) { + List values = ctx.getObjectValues(); + if (values.size() != 1) { + return false; + } + TraceObjectValue val = values.get(0); + if (val.isCanonical() || !val.isObject()) { + return false; + } + return true; + } + + private void activatedFollowLink(DebuggerObjectActionContext ctx) { + List values = ctx.getObjectValues(); + if (values.size() != 1) { + return; + } + setPath(values.get(0).getChild().getCanonicalPath(), null); + } + + private boolean isStepBackwardEnabled(ActionContext ignored) { + if (current.getTrace() == null) { + return false; + } + if (!current.getTime().isSnapOnly()) { + return true; + } + if (current.getSnap() <= 0) { + return false; + } + return true; + } + + private void activatedStepBackward(ActionContext ignored) { + if (current.getTime().isSnapOnly()) { + traceManager.activateSnap(current.getSnap() - 1); + } + else { + traceManager.activateSnap(current.getSnap()); + } + } + + private boolean isStepForwardEnabled(ActionContext ignored) { + Trace curTrace = current.getTrace(); + if (curTrace == null) { + return false; + } + Long maxSnap = curTrace.getTimeManager().getMaxSnap(); + if (maxSnap == null || current.getSnap() >= maxSnap) { + return false; + } + return true; + } + + private void activatedStepForward(ActionContext ignored) { + traceManager.activateSnap(current.getSnap() + 1); + } + + @Override + public JComponent getComponent() { + return mainPanel; + } + + public void coordinatesActivated(DebuggerCoordinates coords) { + this.current = coords; + objectsTreePanel.goToCoordinates(coords); + elementsTablePanel.goToCoordinates(coords); + attributesTablePanel.goToCoordinates(coords); + + checkPath(); + } + + public void traceClosed(Trace trace) { + if (current.getTrace() == trace) { + coordinatesActivated(DebuggerCoordinates.NOWHERE); + } + } + + protected void setPath(TraceObjectKeyPath path, JComponent source) { + if (Objects.equals(this.path, path)) { + return; + } + this.path = path; + if (source != pathField) { + pathField.setText(path.toString()); + } + if (source != objectsTreePanel) { + selectInTree(path); + } + elementsTablePanel.setQuery(ModelQuery.elementsOf(path)); + attributesTablePanel.setQuery(ModelQuery.attributesOf(path)); + + checkPath(); + } + + protected void checkPath() { + if (objectsTreePanel.getNode(path) == null) { + plugin.getTool().setStatusInfo("No such object at path " + path, true); + } + } + + public void setPath(TraceObjectKeyPath path) { + setPath(path, null); + } + + public TraceObjectKeyPath getPath() { + return path; + } + + protected void doSetLimitToCurrentSnap(boolean limitToSnap) { + this.limitToSnap = limitToSnap; + actionLimitToCurrentSnap.setSelected(limitToSnap); + objectsTreePanel.setLimitToSnap(limitToSnap); + elementsTablePanel.setLimitToSnap(limitToSnap); + attributesTablePanel.setLimitToSnap(limitToSnap); + } + + public void setLimitToCurrentSnap(boolean limitToSnap) { + if (this.limitToSnap == limitToSnap) { + return; + } + doSetLimitToCurrentSnap(limitToSnap); + } + + public boolean isLimitToCurrentSnap() { + return limitToSnap; + } + + protected void doSetShowHidden(boolean showHidden) { + this.showHidden = showHidden; + actionShowHidden.setSelected(showHidden); + objectsTreePanel.setShowHidden(showHidden); + elementsTablePanel.setShowHidden(showHidden); + attributesTablePanel.setShowHidden(showHidden); + } + + public void setShowHidden(boolean showHidden) { + if (this.showHidden == showHidden) { + return; + } + doSetShowHidden(showHidden); + } + + public boolean isShowHidden() { + return showHidden; + } + + protected void doSetShowPrimitivesInTree(boolean showPrimitivesInTree) { + this.showPrimitivesInTree = showPrimitivesInTree; + actionShowPrimitivesInTree.setSelected(showPrimitivesInTree); + objectsTreePanel.setShowPrimitives(showPrimitivesInTree); + } + + public void setShowPrimitivesInTree(boolean showPrimitivesInTree) { + if (this.showPrimitivesInTree == showPrimitivesInTree) { + return; + } + doSetShowPrimitivesInTree(showPrimitivesInTree); + } + + public boolean isShowPrimitivesInTree() { + return showPrimitivesInTree; + } + + protected void doSetShowMethodsInTree(boolean showMethodsInTree) { + this.showMethodsInTree = showMethodsInTree; + actionShowMethodsInTree.setSelected(showMethodsInTree); + objectsTreePanel.setShowMethods(showMethodsInTree); + } + + public void setShowMethodsInTree(boolean showMethodsInTree) { + if (this.showMethodsInTree == showMethodsInTree) { + return; + } + doSetShowMethodsInTree(showMethodsInTree); + } + + public boolean isShowMethodsInTree() { + return showMethodsInTree; + } + + @AutoOptionConsumed(name = DebuggerResources.OPTION_NAME_COLORS_VALUE_CHANGED) + public void setDiffColor(Color diffColor) { + if (Objects.equals(this.diffColor, diffColor)) { + return; + } + this.diffColor = diffColor; + objectsTreePanel.setDiffColor(diffColor); + elementsTablePanel.setDiffColor(diffColor); + attributesTablePanel.setDiffColor(diffColor); + } + + @AutoOptionConsumed(name = DebuggerResources.OPTION_NAME_COLORS_VALUE_CHANGED_SEL) + public void setDiffColorSel(Color diffColorSel) { + if (Objects.equals(this.diffColorSel, diffColorSel)) { + return; + } + this.diffColorSel = diffColorSel; + objectsTreePanel.setDiffColorSel(diffColorSel); + elementsTablePanel.setDiffColorSel(diffColorSel); + attributesTablePanel.setDiffColorSel(diffColorSel); + } + + protected void selectInTree(TraceObjectKeyPath path) { + objectsTreePanel.setSelectedKeyPaths(List.of(path)); + } + + @Override + public void writeConfigState(SaveState saveState) { + CONFIG_STATE_HANDLER.writeConfigState(this, saveState); + } + + @Override + public void readConfigState(SaveState saveState) { + CONFIG_STATE_HANDLER.readConfigState(this, saveState); + doSetLimitToCurrentSnap(limitToSnap); + doSetShowHidden(showHidden); + doSetShowPrimitivesInTree(showPrimitivesInTree); + doSetShowMethodsInTree(showMethodsInTree); + } + + @Override + public void writeDataState(SaveState saveState) { + if (isClone) { + current.writeDataState(plugin.getTool(), saveState, KEY_DEBUGGER_COORDINATES); + } + saveState.putString(KEY_PATH, path.toString()); + // TODO? + //GTreeState treeState = objectsTreePanel.tree.getTreeState(); + } + + @Override + public void readDataState(SaveState saveState) { + if (isClone) { + DebuggerCoordinates coords = DebuggerCoordinates.readDataState(plugin.getTool(), + saveState, KEY_DEBUGGER_COORDINATES, true); + if (coords != DebuggerCoordinates.NOWHERE) { + coordinatesActivated(coords); + } + } + setPath(TraceObjectKeyPath.parse(saveState.getString(KEY_PATH, ""))); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerObjectActionContext.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerObjectActionContext.java new file mode 100644 index 0000000000..05afd6d10b --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerObjectActionContext.java @@ -0,0 +1,38 @@ +/* ### + * 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.debug.gui.model; + +import java.awt.Component; +import java.util.Collection; +import java.util.List; + +import docking.ActionContext; +import docking.ComponentProvider; +import ghidra.trace.model.target.TraceObjectValue; + +public class DebuggerObjectActionContext extends ActionContext { + private final List objectValues; + + public DebuggerObjectActionContext(Collection objectValues, + ComponentProvider provider, Component sourceComponent) { + super(provider, sourceComponent); + this.objectValues = List.copyOf(objectValues); + } + + public List getObjectValues() { + return objectValues; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DisplaysModified.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DisplaysModified.java new file mode 100644 index 0000000000..29c0d01366 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DisplaysModified.java @@ -0,0 +1,151 @@ +/* ### + * 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.debug.gui.model; + +import java.util.Objects; + +import com.google.common.collect.Range; + +import ghidra.dbg.util.PathPredicates; +import ghidra.trace.model.Trace; +import ghidra.trace.model.target.TraceObject; +import ghidra.trace.model.target.TraceObjectValue; + +public interface DisplaysModified { + /** + * Get the current trace + * + * @return the trace + */ + Trace getTrace(); + + /** + * Get the current snap + * + * @return the snap + */ + long getSnap(); + + /** + * Get the trace for comparison, which may be the same as the current trace + * + * @return the trace, or null to disable comparison + */ + Trace getDiffTrace(); + + /** + * Get the snap for comparison + * + * @return the snap + */ + long getDiffSnap(); + + /** + * Determine whether two objects differ + * + *

+ * By default the objects are considered equal if their canonical paths agree, without regard to + * the source trace or child values. To compare child values would likely recurse all the way to + * the leaves, which is costly and not exactly informative. This method should only be called + * for objects at the same path, meaning the two objects have at least one path in common. If + * this path is the canonical path, then the two objects (by default) cannot differ. This will + * detect changes in object links, though. + * + * @param newObject the current object + * @param oldObject the previous object + * @return true if the objects differ, i.e., should be displayed in red + */ + default boolean isObjectsDiffer(TraceObject newObject, TraceObject oldObject) { + if (newObject == oldObject) { + return false; + } + return !Objects.equals(newObject.getCanonicalPath(), oldObject.getCanonicalPath()); + } + + /** + * Determine whether two values differ + * + *

+ * By default this defers to the values' Object{@link #equals(Object)} methods, or in case both + * are of type {@link TraceObject}, to {@link #isObjectsDiffer(TraceObject, TraceObject)}. This + * method should only be called for values at the same path. + * + * @param newValue the current value + * @param oldValue the previous value + * @return true if the values differ, i.e., should be displayed in red + */ + default boolean isValuesDiffer(Object newValue, Object oldValue) { + if (newValue instanceof TraceObject && oldValue instanceof TraceObject) { + return isObjectsDiffer((TraceObject) newValue, (TraceObject) oldValue); + } + return !Objects.equals(newValue, oldValue); + } + + /** + * Determine whether two object values (edges) differ + * + *

+ * By default, this behaves as in {@link Objects#equals(Object)}, deferring to + * {@link #isValuesDiffer(Object, Object)}. Note that newEdge can be null because span may + * include more than the current snap. It will be null for edges that are displayed but do not + * contains the current snap. + * + * @param newEdge the current edge, possibly null + * @param oldEdge the previous edge, possibly null + * @return true if the edges' values differ + */ + default boolean isEdgesDiffer(TraceObjectValue newEdge, TraceObjectValue oldEdge) { + if (newEdge == oldEdge) { // Covers case where both are null + return false; + } + if (newEdge == null || oldEdge == null) { + return true; + } + return isValuesDiffer(newEdge.getValue(), oldEdge.getValue()); + } + + default boolean isValueModified(TraceObjectValue value) { + if (value == null || value.getParent() == null) { + return false; + } + Trace diffTrace = getDiffTrace(); + if (diffTrace == null) { + return false; + } + Trace trace = getTrace(); + long snap = getSnap(); + long diffSnap = getDiffSnap(); + if (diffTrace == trace && diffSnap == snap) { + return false; + } + if (diffTrace == trace) { + boolean newContains = value.getLifespan().contains(snap); + boolean oldContains = value.getLifespan().contains(diffSnap); + if (newContains == oldContains) { + return newContains ? isEdgesDiffer(value, value) : true; + } + TraceObjectValue diffEdge = value.getParent().getValue(diffSnap, value.getEntryKey()); + return isEdgesDiffer(newContains ? value : null, diffEdge); + } + TraceObjectValue diffEdge = diffTrace.getObjectManager() + .getValuePaths(Range.singleton(diffSnap), + PathPredicates.pattern(value.getCanonicalPath().getKeyList())) + .findAny() + .map(p -> p.getLastEntry()) + .orElse(null); + return isEdgesDiffer(value, diffEdge); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DisplaysObjectValues.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DisplaysObjectValues.java new file mode 100644 index 0000000000..523c380897 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DisplaysObjectValues.java @@ -0,0 +1,131 @@ +/* ### + * 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.debug.gui.model; + +import ghidra.dbg.target.TargetObject; +import ghidra.trace.model.target.TraceObject; +import ghidra.trace.model.target.TraceObjectValue; +import ghidra.util.HTMLUtilities; + +public interface DisplaysObjectValues { + long getSnap(); + + default String getNullDisplay() { + return ""; + } + + default String getPrimitiveValueDisplay(Object value) { + assert !(value instanceof TraceObject); + assert !(value instanceof TraceObjectValue); + // TODO: Choose decimal or hex for integral types? + if (value == null) { + return getNullDisplay(); + } + return value.toString(); + } + + default String getPrimitiveEdgeType(TraceObjectValue edge) { + return edge.getTargetSchema().getName() + ":" + edge.getValue().getClass().getSimpleName(); + } + + default String getPrimitiveEdgeToolTip(TraceObjectValue edge) { + return getPrimitiveValueDisplay(edge.getValue()) + " (" + getPrimitiveEdgeType(edge) + ")"; + } + + default String getObjectLinkDisplay(TraceObjectValue edge) { + return getObjectDisplay(edge); + } + + default String getObjectType(TraceObjectValue edge) { + TraceObject object = edge.getChild(); + return object.getTargetSchema().getName().toString(); + } + + default String getObjectLinkToolTip(TraceObjectValue edge) { + return "Link to " + getObjectToolTip(edge); + } + + default String getRawObjectDisplay(TraceObjectValue edge) { + TraceObject object = edge.getChild(); + if (object.isRoot()) { + return ""; + } + return object.getCanonicalPath().toString(); + } + + default String getObjectDisplay(TraceObjectValue edge) { + TraceObject object = edge.getChild(); + TraceObjectValue displayAttr = + object.getAttribute(getSnap(), TargetObject.DISPLAY_ATTRIBUTE_NAME); + if (displayAttr != null) { + return displayAttr.getValue().toString(); + } + return getRawObjectDisplay(edge); + } + + default String getObjectToolTip(TraceObjectValue edge) { + String display = getObjectDisplay(edge); + String raw = getRawObjectDisplay(edge); + if (display.equals(raw)) { + return display + " (" + getObjectType(edge) + ")"; + } + return display + " (" + getObjectType(edge) + ":" + raw + ")"; + } + + default String getEdgeDisplay(TraceObjectValue edge) { + if (edge == null) { + return ""; + } + if (edge.isCanonical()) { + return getObjectDisplay(edge); + } + if (edge.isObject()) { + return getObjectLinkDisplay(edge); + } + return getPrimitiveValueDisplay(edge.getValue()); + } + + /** + * Get an HTML string representing how the edge's value should be displayed + * + * @return the display string + */ + default String getEdgeHtmlDisplay(TraceObjectValue edge) { + if (edge == null) { + return ""; + } + if (!edge.isObject()) { + return "" + HTMLUtilities.escapeHTML(getPrimitiveValueDisplay(edge.getValue())); + } + if (edge.isCanonical()) { + return "" + HTMLUtilities.escapeHTML(getObjectDisplay(edge)); + } + return "" + HTMLUtilities.escapeHTML(getObjectLinkDisplay(edge)) + ""; + } + + default String getEdgeToolTip(TraceObjectValue edge) { + if (edge == null) { + return null; + } + if (edge.isCanonical()) { + return getObjectToolTip(edge); + } + if (edge.isObject()) { + return getObjectLinkToolTip(edge); + } + return getPrimitiveEdgeToolTip(edge); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ModelQuery.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ModelQuery.java new file mode 100644 index 0000000000..6a4f5c9ec4 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ModelQuery.java @@ -0,0 +1,165 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.model; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import com.google.common.collect.Range; + +import ghidra.dbg.target.schema.TargetObjectSchema; +import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema; +import ghidra.dbg.util.*; +import ghidra.trace.database.DBTraceUtils; +import ghidra.trace.model.Trace; +import ghidra.trace.model.target.*; + +public class ModelQuery { + // TODO: A more capable query language, e.g., with WHERE clauses. + // Could also want math expressions for the conditionals... Hmm. + // They need to be user enterable, so just a Java API won't suffice. + + public static ModelQuery parse(String queryString) { + return new ModelQuery(PathPredicates.parse(queryString)); + } + + public static ModelQuery elementsOf(TraceObjectKeyPath path) { + return new ModelQuery(new PathPattern(PathUtils.extend(path.getKeyList(), "[]"))); + } + + public static ModelQuery attributesOf(TraceObjectKeyPath path) { + return new ModelQuery(new PathPattern(PathUtils.extend(path.getKeyList(), ""))); + } + + private final PathPredicates predicates; + + /** + * TODO: This should probably be more capable, but for now, just support simple path patterns + * + * @param predicates the patterns + */ + public ModelQuery(PathPredicates predicates) { + this.predicates = predicates; + } + + @Override + public String toString() { + return ""; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof ModelQuery)) { + return false; + } + ModelQuery that = (ModelQuery) obj; + if (!Objects.equals(this.predicates, that.predicates)) { + return false; + } + return true; + } + + /** + * Render the query as a string as in {@link #parse(String)} + * + * @return the string + */ + public String toQueryString() { + return predicates.getSingletonPattern().toPatternString(); + } + + /** + * Execute the query + * + * @param trace the data source + * @param span the span of snapshots to search, usually all or a singleton + * @return the stream of resulting objects + */ + public Stream streamObjects(Trace trace, Range span) { + TraceObjectManager objects = trace.getObjectManager(); + TraceObject root = objects.getRootObject(); + return objects.getValuePaths(span, predicates) + .map(p -> p.getDestinationValue(root)) + .filter(v -> v instanceof TraceObject) + .map(v -> (TraceObject) v); + } + + public Stream streamValues(Trace trace, Range span) { + TraceObjectManager objects = trace.getObjectManager(); + return objects.getValuePaths(span, predicates).map(p -> { + TraceObjectValue last = p.getLastEntry(); + return last == null ? objects.getRootObject().getCanonicalParent(0) : last; + }); + } + + public Stream streamPaths(Trace trace, Range span) { + return trace.getObjectManager().getValuePaths(span, predicates).map(p -> p); + } + + /** + * Compute the named attributes for resulting objects, according to the schema + * + *

+ * This does not include the "default attribute schema." + * + * @param trace the data source + * @return the list of attributes + */ + public Stream computeAttributes(Trace trace) { + TraceObjectManager objects = trace.getObjectManager(); + TargetObjectSchema schema = + objects.getRootSchema().getSuccessorSchema(predicates.getSingletonPattern().asPath()); + return schema.getAttributeSchemas() + .values() + .stream() + .filter(as -> !"".equals(as.getName())); + } + + /** + * Determine whether this query would include the given value in its result + * + *

+ * More precisely, determine whether it would traverse the given value, accept it, and include + * its child in the result. It's possible the child could be included via another value, but + * this only considers the given value. + * + * @param span the span to consider + * @param value the value to examine + * @return true if the value would be accepted + */ + public boolean includes(Range span, TraceObjectValue value) { + List path = predicates.getSingletonPattern().asPath(); + if (path.isEmpty()) { + return value.getParent() == null; + } + if (!PathPredicates.keyMatches(PathUtils.getKey(path), value.getEntryKey())) { + return false; + } + if (!DBTraceUtils.intersect(span, value.getLifespan())) { + return false; + } + TraceObject parent = value.getParent(); + if (parent == null) { + return false; + } + return parent.getAncestors(span, predicates.removeRight(1)) + .anyMatch(v -> v.getSource(parent).isRoot()); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTableModel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTableModel.java new file mode 100644 index 0000000000..157bddde06 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTableModel.java @@ -0,0 +1,412 @@ +/* ### + * 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.debug.gui.model; + +import java.awt.Color; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.google.common.collect.*; + +import docking.widgets.table.DynamicTableColumn; +import docking.widgets.table.TableColumnDescriptor; +import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow; +import ghidra.app.plugin.core.debug.gui.model.columns.*; +import ghidra.dbg.target.schema.SchemaContext; +import ghidra.dbg.target.schema.TargetObjectSchema; +import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema; +import ghidra.framework.plugintool.Plugin; +import ghidra.trace.model.Trace; +import ghidra.trace.model.target.TraceObject; +import ghidra.trace.model.target.TraceObjectValue; +import ghidra.util.HTMLUtilities; + +public class ObjectTableModel extends AbstractQueryTableModel { + /** Initialized in {@link #createTableColumnDescriptor()}, which precedes this. */ + private TraceValueValColumn valueColumn; + private TraceValueLifePlotColumn lifePlotColumn; + + protected static Stream distinctCanonical( + Stream stream) { + Set seen = new HashSet<>(); + return stream.filter(value -> { + if (!value.isCanonical()) { + return true; + } + return seen.add(value.getChild()); + }); + } + + public interface ValueRow { + String getKey(); + + RangeSet getLife(); + + TraceObjectValue getValue(); + + /** + * Get a non-HTML string representing how this row's value should be sorted, filtered, etc. + * + * @return the display string + */ + String getDisplay(); + + /** + * Get an HTML string representing how this row's value should be displayed + * + * @return the display string + */ + String getHtmlDisplay(); + + String getToolTip(); + + /** + * Determine whether the value in the row has changed since the diff coordinates + * + * @return true if they differ, i.e., should be rendered in red + */ + boolean isModified(); + + TraceObjectValue getAttribute(String attributeName); + + String getAttributeDisplay(String attributeName); + + String getAttributeHtmlDisplay(String attributeName); + + String getAttributeToolTip(String attributeName); + + boolean isAttributeModified(String attributeName); + + } + + protected abstract class AbstractValueRow implements ValueRow { + protected final TraceObjectValue value; + + public AbstractValueRow(TraceObjectValue value) { + this.value = value; + } + + @Override + public TraceObjectValue getValue() { + return value; + } + + @Override + public String getKey() { + return value.getEntryKey(); + } + + @Override + public RangeSet getLife() { + RangeSet life = TreeRangeSet.create(); + life.add(value.getLifespan()); + return life; + } + + @Override + public boolean isModified() { + return isValueModified(getValue()); + } + } + + protected class PrimitiveRow extends AbstractValueRow { + public PrimitiveRow(TraceObjectValue value) { + super(value); + } + + @Override + public String getDisplay() { + return display.getPrimitiveValueDisplay(value.getValue()); + } + + @Override + public String getHtmlDisplay() { + return "" + + HTMLUtilities.escapeHTML(display.getPrimitiveValueDisplay(value.getValue())); + } + + @Override + public String getToolTip() { + return display.getPrimitiveEdgeToolTip(value); + } + + @Override + public TraceObjectValue getAttribute(String attributeName) { + return null; + } + + @Override + public String getAttributeDisplay(String attributeName) { + return null; + } + + @Override + public String getAttributeHtmlDisplay(String attributeName) { + return null; + } + + @Override + public String getAttributeToolTip(String attributeName) { + return null; + } + + @Override + public boolean isAttributeModified(String attributeName) { + return false; + } + } + + protected class ObjectRow extends AbstractValueRow { + private final TraceObject object; + + public ObjectRow(TraceObjectValue value) { + super(value); + this.object = value.getChild(); + } + + public TraceObject getTraceObject() { + return object; + } + + @Override + public String getDisplay() { + return display.getEdgeDisplay(value); + } + + @Override + public String getHtmlDisplay() { + return display.getEdgeHtmlDisplay(value); + } + + @Override + public String getToolTip() { + return display.getEdgeToolTip(value); + } + + @Override + public TraceObjectValue getAttribute(String attributeName) { + return object.getAttribute(getSnap(), attributeName); + } + + @Override + public String getAttributeDisplay(String attributeName) { + return display.getEdgeDisplay(getAttribute(attributeName)); + } + + @Override + public String getAttributeHtmlDisplay(String attributeName) { + return display.getEdgeHtmlDisplay(getAttribute(attributeName)); + } + + @Override + public String getAttributeToolTip(String attributeName) { + return display.getEdgeToolTip(getAttribute(attributeName)); + } + + @Override + public boolean isAttributeModified(String attributeName) { + return isValueModified(getAttribute(attributeName)); + } + } + + protected ValueRow rowForValue(TraceObjectValue value) { + if (value.getValue() instanceof TraceObject) { + return new ObjectRow(value); + } + return new PrimitiveRow(value); + } + + protected static class ColKey { + public static ColKey fromSchema(SchemaContext ctx, AttributeSchema attributeSchema) { + String name = attributeSchema.getName(); + Class type = TraceValueObjectAttributeColumn.computeColumnType(ctx, attributeSchema); + return new ColKey(name, type); + } + + private final String name; + private final Class type; + private final int hash; + + public ColKey(String name, Class type) { + this.name = name; + this.type = type; + this.hash = Objects.hash(name, type); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof ColKey)) { + return false; + } + ColKey that = (ColKey) obj; + if (!Objects.equals(this.name, that.name)) { + return false; + } + if (this.type != that.type) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return hash; + } + } + + // TODO: Save and restore these between sessions, esp., their settings + private Map columnCache = new HashMap<>(); + + protected ObjectTableModel(Plugin plugin) { + super("Object Model", plugin); + } + + @Override + protected void traceChanged() { + reloadAttributeColumns(); + updateTimelineMax(); + super.traceChanged(); + } + + @Override + protected void queryChanged() { + reloadAttributeColumns(); + super.queryChanged(); + } + + @Override + protected void showHiddenChanged() { + reloadAttributeColumns(); + super.showHiddenChanged(); + } + + @Override + protected void maxSnapChanged() { + updateTimelineMax(); + refresh(); + } + + protected void updateTimelineMax() { + Long max = getTrace() == null ? null : getTrace().getTimeManager().getMaxSnap(); + Range fullRange = Range.closed(0L, max == null ? 1 : max + 1); + lifePlotColumn.setFullRange(fullRange); + } + + protected List computeAttributeSchemas() { + Trace trace = getTrace(); + ModelQuery query = getQuery(); + if (trace == null || query == null) { + return List.of(); + } + TargetObjectSchema rootSchema = trace.getObjectManager().getRootSchema(); + if (rootSchema == null) { + return List.of(); + } + SchemaContext ctx = rootSchema.getContext(); + return query.computeAttributes(trace) + .filter(a -> isShowHidden() || !a.isHidden()) + .filter(a -> !ctx.getSchema(a.getSchema()).isCanonicalContainer()) + .collect(Collectors.toList()); + } + + protected void reloadAttributeColumns() { + List attributes; + Trace trace = getTrace(); + ModelQuery query = getQuery(); + if (trace == null || query == null || trace.getObjectManager().getRootSchema() == null) { + attributes = List.of(); + } + else { + SchemaContext ctx = trace.getObjectManager().getRootSchema().getContext(); + attributes = query.computeAttributes(trace) + .filter(a -> isShowHidden() || !a.isHidden()) + .filter(a -> !ctx.getSchema(a.getSchema()).isCanonicalContainer()) + .collect(Collectors.toList()); + } + resyncAttributeColumns(attributes); + } + + protected Set> computeAttributeColumns( + Collection attributes) { + Trace trace = getTrace(); + if (trace == null) { + return Set.of(); + } + TargetObjectSchema rootSchema = trace.getObjectManager().getRootSchema(); + if (rootSchema == null) { + return Set.of(); + } + SchemaContext ctx = rootSchema.getContext(); + return attributes.stream() + .map(as -> columnCache.computeIfAbsent(ColKey.fromSchema(ctx, as), + ck -> TraceValueObjectAttributeColumn.fromSchema(ctx, as))) + .collect(Collectors.toSet()); + } + + protected void resyncAttributeColumns(Collection attributes) { + Set> columns = + new HashSet<>(computeAttributeColumns(attributes)); + Set> toRemove = new HashSet<>(); + for (int i = 0; i < getColumnCount(); i++) { + DynamicTableColumn exists = getColumn(i); + if (!(exists instanceof TraceValueObjectAttributeColumn)) { + continue; + } + if (!columns.remove(exists)) { + toRemove.add(exists); + } + } + removeTableColumns(toRemove); + addTableColumns(columns); + } + + @Override + protected Stream streamRows(Trace trace, ModelQuery query, Range span) { + return distinctCanonical(query.streamValues(trace, span) + .filter(v -> isShowHidden() || !v.isHidden())) + .map(this::rowForValue); + } + + @Override + protected TableColumnDescriptor createTableColumnDescriptor() { + TableColumnDescriptor descriptor = new TableColumnDescriptor<>(); + descriptor.addVisibleColumn(new TraceValueKeyColumn()); + descriptor.addVisibleColumn(valueColumn = new TraceValueValColumn()); + descriptor.addVisibleColumn(new TraceValueLifeColumn()); + descriptor.addHiddenColumn(lifePlotColumn = new TraceValueLifePlotColumn()); + return descriptor; + } + + @Override + public void setDiffColor(Color diffColor) { + valueColumn.setDiffColor(diffColor); + for (TraceValueObjectAttributeColumn column : columnCache.values()) { + column.setDiffColor(diffColor); + } + } + + @Override + public void setDiffColorSel(Color diffColorSel) { + valueColumn.setDiffColorSel(diffColorSel); + for (TraceValueObjectAttributeColumn column : columnCache.values()) { + column.setDiffColorSel(diffColorSel); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTreeModel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTreeModel.java new file mode 100644 index 0000000000..31cd0c4034 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTreeModel.java @@ -0,0 +1,777 @@ +/* ### + * 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.debug.gui.model; + +import java.util.*; +import java.util.stream.Collectors; + +import javax.swing.Icon; + +import com.google.common.collect.Range; + +import docking.widgets.tree.GTreeLazyNode; +import docking.widgets.tree.GTreeNode; +import ghidra.app.plugin.core.debug.gui.DebuggerResources; +import ghidra.dbg.target.*; +import ghidra.trace.database.DBTraceUtils; +import ghidra.trace.model.Trace; +import ghidra.trace.model.Trace.TraceObjectChangeType; +import ghidra.trace.model.TraceDomainObjectListener; +import ghidra.trace.model.target.*; +import ghidra.util.HTMLUtilities; +import ghidra.util.datastruct.WeakValueHashMap; +import utilities.util.IDKeyed; + +public class ObjectTreeModel implements DisplaysModified { + + class ListenerForChanges extends TraceDomainObjectListener { + public ListenerForChanges() { + listenFor(TraceObjectChangeType.CREATED, this::objectCreated); + listenFor(TraceObjectChangeType.VALUE_CREATED, this::valueCreated); + listenFor(TraceObjectChangeType.VALUE_DELETED, this::valueDeleted); + listenFor(TraceObjectChangeType.VALUE_LIFESPAN_CHANGED, this::valueLifespanChanged); + } + + protected boolean isEventValue(TraceObjectValue value) { + if (!value.getParent() + .getTargetSchema() + .getInterfaces() + .contains(TargetEventScope.class)) { + return false; + } + if (!TargetEventScope.EVENT_OBJECT_ATTRIBUTE_NAME.equals(value.getEntryKey())) { + return false; + } + return true; + } + + protected boolean isEnabledValue(TraceObjectValue value) { + Set> interfaces = + value.getParent().getTargetSchema().getInterfaces(); + if (!interfaces.contains(TargetBreakpointSpec.class) && + !interfaces.contains(TargetBreakpointLocation.class)) { + return false; + } + if (!TargetBreakpointSpec.ENABLED_ATTRIBUTE_NAME.equals(value.getEntryKey())) { + return false; + } + return true; + } + + private void objectCreated(TraceObject object) { + if (object.isRoot()) { + reload(); + } + } + + private void valueCreated(TraceObjectValue value) { + if (!DBTraceUtils.intersect(value.getLifespan(), span)) { + return; + } + AbstractNode node = nodeCache.getByObject(value.getParent()); + if (node == null) { + return; + } + if (isEventValue(value)) { + refresh(); + } + if (isEnabledValue(value)) { + node.fireNodeChanged(); + } + node.childCreated(value); + } + + private void valueDeleted(TraceObjectValue value) { + if (!DBTraceUtils.intersect(value.getLifespan(), span)) { + return; + } + AbstractNode node = nodeCache.getByObject(value.getParent()); + if (node == null) { + return; + } + if (isEventValue(value)) { + refresh(); + } + if (isEnabledValue(value)) { + node.fireNodeChanged(); + } + node.childDeleted(value); + } + + private void valueLifespanChanged(TraceObjectValue value, Range oldSpan, + Range newSpan) { + boolean inOld = DBTraceUtils.intersect(oldSpan, span); + boolean inNew = DBTraceUtils.intersect(newSpan, span); + if (inOld == inNew) { + return; + } + AbstractNode node = nodeCache.getByObject(value.getParent()); + if (node == null) { + return; + } + if (isEventValue(value)) { + refresh(); + } + if (isEnabledValue(value)) { + node.fireNodeChanged(); + } + if (inNew) { + node.childCreated(value); + } + else { + node.childDeleted(value); + } + } + } + + class NodeCache { + Map, AbstractNode> byValue = new WeakValueHashMap<>(); + Map, AbstractNode> byObject = new WeakValueHashMap<>(); + + protected AbstractNode createNode(TraceObjectValue value) { + if (value.isCanonical()) { + return new CanonicalNode(value); + } + if (value.isObject()) { + return new LinkNode(value); + } + return new PrimitiveNode(value); + } + + protected AbstractNode getOrCreateNode(TraceObjectValue value) { + if (value.getParent() == null) { + return root; + } + AbstractNode node = + byValue.computeIfAbsent(new IDKeyed<>(value), k -> createNode(value)); + //AbstractNode node = createNode(value); + if (value.isCanonical()) { + byObject.put(new IDKeyed<>(value.getChild()), node); + } + return node; + } + + protected AbstractNode getByValue(TraceObjectValue value) { + return byValue.get(new IDKeyed<>(value)); + } + + protected AbstractNode getByObject(TraceObject object) { + if (object.isRoot()) { + return root; + } + return byObject.get(new IDKeyed<>(object)); + } + } + + public abstract class AbstractNode extends GTreeLazyNode { + public abstract TraceObjectValue getValue(); + + protected void childCreated(TraceObjectValue value) { + if (getParent() == null || !isLoaded()) { + return; + } + if (isValueVisible(value)) { + AbstractNode child = nodeCache.getOrCreateNode(value); + addNode(child); + } + } + + protected void childDeleted(TraceObjectValue value) { + if (getParent() == null || !isLoaded()) { + return; + } + AbstractNode child = nodeCache.getByValue(value); + if (child != null) { + removeNode(child); + } + } + + protected AbstractNode getNode(TraceObjectKeyPath p, int pos) { + if (pos >= p.getKeyList().size()) { + return this; + } + String key = p.getKeyList().get(pos); + AbstractNode matched = children().stream() + .map(c -> (AbstractNode) c) + .filter(c -> key.equals(c.getValue().getEntryKey())) + .findFirst() + .orElse(null); + if (matched == null) { + return null; + } + return matched.getNode(p, pos + 1); + } + + public AbstractNode getNode(TraceObjectKeyPath p) { + return getNode(p, 0); + } + + protected boolean isModified() { + return isValueModified(getValue()); + } + } + + class RootNode extends AbstractNode { + @Override + public TraceObjectValue getValue() { + if (trace == null) { + return null; + } + TraceObject root = trace.getObjectManager().getRootObject(); + if (root == null) { + return null; + } + return root.getCanonicalParent(0); + } + + @Override + public String getName() { + if (trace == null) { + return "No trace is active"; + } + TraceObject root = trace.getObjectManager().getRootObject(); + if (root == null) { + return "Trace has no model"; + } + return "" + + HTMLUtilities.escapeHTML(display.getObjectDisplay(root.getCanonicalParent(0))); + } + + @Override + public Icon getIcon(boolean expanded) { + return DebuggerResources.ICON_DEBUGGER; // TODO + } + + @Override + public String getToolTip() { + if (trace == null) { + return "No trace is active"; + } + TraceObject root = trace.getObjectManager().getRootObject(); + if (root == null) { + return "Trace has no model"; + } + return display.getObjectToolTip(root.getCanonicalParent(0)); + } + + @Override + public boolean isLeaf() { + return false; + } + + @Override + protected List generateChildren() { + if (trace == null) { + return List.of(); + } + TraceObject root = trace.getObjectManager().getRootObject(); + if (root == null) { + return List.of(); + } + return generateObjectChildren(root); + } + + @Override + protected boolean isModified() { + return false; + } + + @Override + protected void childCreated(TraceObjectValue value) { + unloadChildren(); + } + } + + public class PrimitiveNode extends AbstractNode { + protected final TraceObjectValue value; + + public PrimitiveNode(TraceObjectValue value) { + this.value = value; + } + + @Override + public TraceObjectValue getValue() { + return value; + } + + @Override + protected List generateChildren() { + return List.of(); + } + + @Override + public String getName() { + String html = HTMLUtilities.escapeHTML( + value.getEntryKey() + ": " + display.getPrimitiveValueDisplay(value.getValue())); + return "" + html; + } + + @Override + public Icon getIcon(boolean expanded) { + return DebuggerResources.ICON_OBJECT_UNPOPULATED; + } + + @Override + public String getToolTip() { + return display.getPrimitiveEdgeToolTip(value); + } + + @Override + public boolean isLeaf() { + return true; + } + } + + public abstract class AbstractObjectNode extends AbstractNode { + protected final TraceObjectValue value; + protected final TraceObject object; + + public AbstractObjectNode(TraceObjectValue value) { + this.value = value; + this.object = Objects.requireNonNull(value.getChild()); + } + + @Override + public TraceObjectValue getValue() { + return value; + } + + @Override + public Icon getIcon(boolean expanded) { + return getObjectIcon(value, expanded); + } + } + + public class LinkNode extends AbstractObjectNode { + public LinkNode(TraceObjectValue value) { + super(value); + } + + @Override + public String getName() { + return "" + HTMLUtilities.escapeHTML(value.getEntryKey()) + ": " + + HTMLUtilities.escapeHTML(display.getObjectLinkDisplay(value)) + ""; + } + + @Override + public String getToolTip() { + return display.getObjectLinkToolTip(value); + } + + @Override + public boolean isLeaf() { + return true; + } + + @Override + protected List generateChildren() { + return List.of(); + } + + @Override + protected void childCreated(TraceObjectValue value) { + throw new AssertionError(); + } + + @Override + protected void childDeleted(TraceObjectValue value) { + throw new AssertionError(); + } + } + + public class CanonicalNode extends AbstractObjectNode { + public CanonicalNode(TraceObjectValue value) { + super(value); + } + + @Override + protected List generateChildren() { + return generateObjectChildren(object); + } + + @Override + public String getName() { + return "" + HTMLUtilities.escapeHTML(display.getObjectDisplay(value)); + } + + @Override + public String getToolTip() { + return display.getObjectToolTip(value); + } + + @Override + public Icon getIcon(boolean expanded) { + TraceObjectValue parentValue = object.getCanonicalParent(snap); + if (parentValue == null) { + return super.getIcon(expanded); + } + if (!parentValue.getParent().getTargetSchema().isCanonicalContainer()) { + return super.getIcon(expanded); + } + if (!isOnEventPath(object)) { + return super.getIcon(expanded); + } + return DebuggerResources.ICON_EVENT_MARKER; + } + + @Override + public boolean isLeaf() { + return false; + } + } + + interface LastKeyDisplaysObjectValues extends DisplaysObjectValues { + @Override + default String getRawObjectDisplay(TraceObjectValue edge) { + TraceObject object = edge.getChild(); + if (object.isRoot()) { + return "Root"; + } + if (edge.isCanonical()) { + return edge.getEntryKey(); + } + return object.getCanonicalPath().toString(); + } + } + + protected class TreeDisplaysObjectValues implements LastKeyDisplaysObjectValues { + @Override + public long getSnap() { + return snap; + } + } + + protected class DiffTreeDisplaysObjectValues implements LastKeyDisplaysObjectValues { + @Override + public long getSnap() { + return diffSnap; + } + } + + private Trace trace; + private long snap; + private Trace diffTrace; + private long diffSnap; + private Range span = Range.all(); + private boolean showHidden; + private boolean showPrimitives; + private boolean showMethods; + + private final RootNode root = new RootNode(); + private final NodeCache nodeCache = new NodeCache(); + + // TODO: User-modifiable? + // TODO: Load and save this. Options panel? Defaults for GDB/dbgeng? + private Map icons = fillIconMap(new HashMap<>()); + + private final ListenerForChanges listenerForChanges = newListenerForChanges(); + protected final DisplaysObjectValues display = new TreeDisplaysObjectValues(); + protected final DisplaysObjectValues diffDisplay = new DiffTreeDisplaysObjectValues(); + + protected ListenerForChanges newListenerForChanges() { + return new ListenerForChanges(); + } + + protected Map fillIconMap(Map map) { + map.put("Process", DebuggerResources.ICON_PROCESS); + map.put("Thread", DebuggerResources.ICON_THREAD); + map.put("Memory", DebuggerResources.ICON_REGIONS); + map.put("Interpreter", DebuggerResources.ICON_CONSOLE); + map.put("Console", DebuggerResources.ICON_CONSOLE); + map.put("Stack", DebuggerResources.ICON_PROVIDER_STACK); + // TODO: StackFrame + map.put("BreakpointContainer", DebuggerResources.ICON_BREAKPOINTS); + map.put("BreakpointLocationContainer", DebuggerResources.ICON_BREAKPOINTS); + // NOTE: Breakpoints done dynamically for enabled/disabled. + map.put("RegisterContainer", DebuggerResources.ICON_REGISTERS); + // TODO: Register + map.put("ModuleContainer", DebuggerResources.ICON_MODULES); + // TODO: single module / section + return map; + } + + protected TraceObject getEventObject(TraceObject object) { + TraceObject scope = object.queryCanonicalAncestorsTargetInterface(TargetEventScope.class) + .findFirst() + .orElse(null); + if (scope == null) { + return null; + } + if (scope == object) { + return null; + } + TraceObjectValue eventValue = + scope.getAttribute(snap, TargetEventScope.EVENT_OBJECT_ATTRIBUTE_NAME); + if (eventValue == null || !eventValue.isObject()) { + return null; + } + return eventValue.getChild(); + } + + protected boolean isOnEventPath(TraceObject object) { + TraceObject eventObject = getEventObject(object); + if (eventObject == null) { + return false; + } + if (object.getCanonicalPath().isAncestor(eventObject.getCanonicalPath())) { + return true; + } + return false; + } + + protected Icon getObjectIcon(TraceObjectValue edge, boolean expanded) { + String type = display.getObjectType(edge); + Icon forType = icons.get(type); + if (forType != null) { + return forType; + } + if (type.contains("Breakpoint")) { + TraceObject object = edge.getChild(); + TraceObjectValue en = + object.getAttribute(snap, TargetBreakpointSpec.ENABLED_ATTRIBUTE_NAME); + // includes true or non-boolean values + if (en == null || !Objects.equals(false, en.getValue())) { + return DebuggerResources.ICON_SET_BREAKPOINT; + } + return DebuggerResources.ICON_DISABLE_BREAKPOINT; + } + return DebuggerResources.ICON_OBJECT_POPULATED; + /* + * TODO?: Populated/unpopulated? Seems to duplicate isLeaf. The absence/presence of an + * expander should already communicate this info.... We could instead use icon to indicate + * freshness, but how would we know? The sync mode from the schema might help. + */ + } + + protected boolean isValueVisible(TraceObjectValue value) { + if (!showHidden && value.isHidden()) { + return false; + } + if (!showPrimitives && !value.isObject()) { + return false; + } + if (!showMethods && value.isObject() && value.getChild().isMethod(snap)) { + return false; + } + if (!DBTraceUtils.intersect(value.getLifespan(), span)) { + return false; + } + return true; + } + + @Override + public boolean isEdgesDiffer(TraceObjectValue newEdge, TraceObjectValue oldEdge) { + if (DisplaysModified.super.isEdgesDiffer(newEdge, oldEdge)) { + return true; + } + // Hack to incorporate _display logic to differencing. + // This ensures "boxed" primitives show as differing at the object level + return !Objects.equals(diffDisplay.getEdgeDisplay(oldEdge), + display.getEdgeDisplay(newEdge)); + } + + protected List generateObjectChildren(TraceObject object) { + List result = ObjectTableModel + .distinctCanonical(object.getValues().stream().filter(this::isValueVisible)) + .map(v -> nodeCache.getOrCreateNode(v)) + .collect(Collectors.toList()); + return result; + } + + public GTreeLazyNode getRoot() { + return root; + } + + protected void removeOldListeners() { + if (trace != null) { + trace.removeListener(listenerForChanges); + } + } + + protected void addNewListeners() { + if (trace != null) { + trace.addListener(listenerForChanges); + } + } + + protected void refresh() { + for (AbstractNode node : nodeCache.byObject.values()) { + node.fireNodeChanged(); + } + } + + protected void reload() { + root.unloadChildren(); + } + + public void setTrace(Trace trace) { + if (this.trace == trace) { + return; + } + removeOldListeners(); + this.trace = trace; + addNewListeners(); + traceChanged(); + } + + protected void traceChanged() { + reload(); + } + + @Override + public Trace getTrace() { + return trace; + } + + protected void snapChanged() { + // Span will be set to singleton by client, if desired + refresh(); + } + + public void setSnap(long snap) { + if (this.snap == snap) { + return; + } + this.snap = snap; + snapChanged(); + } + + @Override + public long getSnap() { + return snap; + } + + protected void diffTraceChanged() { + refresh(); + } + + /** + * Set alternative trace to colorize values that differ + * + *

+ * The same trace can be used, but with an alternative snap, if desired. See + * {@link #setDiffSnap(long)}. One common use is to compare with the previous snap of the same + * trace. Another common use is to compare with the previous navigation. + * + * @param diffTrace the alternative trace + */ + public void setDiffTrace(Trace diffTrace) { + if (this.diffTrace == diffTrace) { + return; + } + this.diffTrace = diffTrace; + diffTraceChanged(); + } + + @Override + public Trace getDiffTrace() { + return diffTrace; + } + + protected void diffSnapChanged() { + refresh(); + } + + /** + * Set alternative snap to colorize values that differ + * + *

+ * The diff trace must be set, even if it's the same as the trace being displayed. See + * {@link #setDiffTrace(Trace)}. + * + * @param diffSnap the alternative snap + */ + public void setDiffSnap(long diffSnap) { + if (this.diffSnap == diffSnap) { + return; + } + this.diffSnap = diffSnap; + diffSnapChanged(); + } + + @Override + public long getDiffSnap() { + return diffSnap; + } + + protected void spanChanged() { + reload(); + } + + public void setSpan(Range span) { + if (Objects.equals(this.span, span)) { + return; + } + this.span = span; + spanChanged(); + } + + public Range getSpan() { + return span; + } + + protected void showHiddenChanged() { + reload(); + } + + public void setShowHidden(boolean showHidden) { + if (this.showHidden == showHidden) { + return; + } + this.showHidden = showHidden; + showHiddenChanged(); + } + + public boolean isShowHidden() { + return showHidden; + } + + protected void showPrimitivesChanged() { + reload(); + } + + public void setShowPrimitives(boolean showPrimitives) { + if (this.showPrimitives == showPrimitives) { + return; + } + this.showPrimitives = showPrimitives; + showPrimitivesChanged(); + } + + public boolean isShowPrimitives() { + return showPrimitives; + } + + protected void showMethodsChanged() { + reload(); + } + + public void setShowMethods(boolean showMethods) { + if (this.showMethods == showMethods) { + return; + } + this.showMethods = showMethods; + showMethodsChanged(); + } + + public boolean isShowMethods() { + return showMethods; + } + + public AbstractNode getNode(TraceObjectKeyPath p) { + return root.getNode(p); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTablePanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTablePanel.java new file mode 100644 index 0000000000..5bdb24ebe2 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTablePanel.java @@ -0,0 +1,30 @@ +/* ### + * 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.debug.gui.model; + +import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow; +import ghidra.framework.plugintool.Plugin; + +public class ObjectsTablePanel extends AbstractQueryTablePanel { + public ObjectsTablePanel(Plugin plugin) { + super(plugin); + } + + @Override + protected AbstractQueryTableModel createModel(Plugin plugin) { + return new ObjectTableModel(plugin); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTreePanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTreePanel.java new file mode 100644 index 0000000000..8925d0c566 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTreePanel.java @@ -0,0 +1,265 @@ +/* ### + * 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.debug.gui.model; + +import java.awt.*; +import java.awt.event.MouseListener; +import java.util.*; +import java.util.List; +import java.util.stream.*; + +import javax.swing.JPanel; +import javax.swing.JTree; +import javax.swing.tree.TreePath; + +import com.google.common.collect.Range; + +import docking.widgets.tree.*; +import docking.widgets.tree.support.GTreeRenderer; +import docking.widgets.tree.support.GTreeSelectionListener; +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.plugin.core.debug.gui.DebuggerResources; +import ghidra.app.plugin.core.debug.gui.model.ObjectTreeModel.AbstractNode; +import ghidra.trace.model.target.TraceObjectKeyPath; + +public class ObjectsTreePanel extends JPanel { + + protected class ObjectsTreeRenderer extends GTreeRenderer implements ColorsModified.InTree { + { + setHTMLRenderingEnabled(true); + } + + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, + boolean expanded, boolean leaf, int row, boolean hasFocus) { + super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, + hasFocus); + if (!(value instanceof AbstractNode)) { + return this; + } + + AbstractNode node = (AbstractNode) value; + setForeground(getForegroundFor(tree, node.isModified(), selected)); + return this; + } + + @Override + public Color getDiffForeground(JTree tree) { + return diffColor; + } + + @Override + public Color getDiffSelForeground(JTree tree) { + return diffColorSel; + } + } + + protected final ObjectTreeModel treeModel; + protected final GTree tree; + + protected DebuggerCoordinates current = DebuggerCoordinates.NOWHERE; + protected boolean limitToSnap = true; + protected boolean showHidden = false; + protected boolean showPrimitives = false; + protected boolean showMethods = false; + + protected Color diffColor = DebuggerResources.DEFAULT_COLOR_VALUE_CHANGED; + protected Color diffColorSel = DebuggerResources.DEFAULT_COLOR_VALUE_CHANGED_SEL; + + public ObjectsTreePanel() { + super(new BorderLayout()); + treeModel = createModel(); + tree = new GTree(treeModel.getRoot()); + + tree.setCellRenderer(new ObjectsTreeRenderer()); + add(tree, BorderLayout.CENTER); + } + + protected ObjectTreeModel createModel() { + return new ObjectTreeModel(); + } + + protected class KeepTreeState implements AutoCloseable { + private final GTreeState state; + + public KeepTreeState() { + this.state = tree.getTreeState(); + } + + @Override + public void close() { + tree.restoreTreeState(state); + } + } + + public void goToCoordinates(DebuggerCoordinates coords) { + // TODO: thread should probably become a TraceObject once we transition + if (DebuggerCoordinates.equalsIgnoreRecorderAndView(current, coords)) { + return; + } + DebuggerCoordinates previous = current; + this.current = coords; + try (KeepTreeState keep = new KeepTreeState()) { + treeModel.setDiffTrace(previous.getTrace()); + treeModel.setTrace(current.getTrace()); + treeModel.setDiffSnap(previous.getSnap()); + treeModel.setSnap(current.getSnap()); + if (limitToSnap) { + treeModel.setSpan(Range.singleton(current.getSnap())); + } + tree.filterChanged(); + } + } + + public void setLimitToSnap(boolean limitToSnap) { + if (this.limitToSnap == limitToSnap) { + return; + } + this.limitToSnap = limitToSnap; + try (KeepTreeState keep = new KeepTreeState()) { + treeModel.setSpan(limitToSnap ? Range.singleton(current.getSnap()) : Range.all()); + } + } + + public boolean isLimitToSnap() { + return limitToSnap; + } + + public void setShowHidden(boolean showHidden) { + if (this.showHidden == showHidden) { + return; + } + this.showHidden = showHidden; + try (KeepTreeState keep = new KeepTreeState()) { + treeModel.setShowHidden(showHidden); + } + } + + public boolean isShowHidden() { + return showHidden; + } + + public void setShowPrimitives(boolean showPrimitives) { + if (this.showPrimitives == showPrimitives) { + return; + } + this.showPrimitives = showPrimitives; + try (KeepTreeState keep = new KeepTreeState()) { + treeModel.setShowPrimitives(showPrimitives); + } + } + + public boolean isShowPrimitives() { + return showPrimitives; + } + + public void setShowMethods(boolean showMethods) { + if (this.showMethods == showMethods) { + return; + } + this.showMethods = showMethods; + try (KeepTreeState keep = new KeepTreeState()) { + treeModel.setShowMethods(showMethods); + } + } + + public boolean isShowMethods() { + return showMethods; + } + + public void setDiffColor(Color diffColor) { + if (Objects.equals(this.diffColor, diffColor)) { + return; + } + this.diffColor = diffColor; + repaint(); + } + + public void setDiffColorSel(Color diffColorSel) { + if (Objects.equals(this.diffColorSel, diffColorSel)) { + return; + } + this.diffColorSel = diffColorSel; + repaint(); + } + + public void addTreeSelectionListener(GTreeSelectionListener listener) { + tree.addGTreeSelectionListener(listener); + } + + public void removeTreeSelectionListener(GTreeSelectionListener listener) { + tree.removeGTreeSelectionListener(listener); + } + + @Override + public synchronized void addMouseListener(MouseListener l) { + super.addMouseListener(l); + // Is this a HACK? + tree.addMouseListener(l); + } + + @Override + public synchronized void removeMouseListener(MouseListener l) { + super.removeMouseListener(l); + // HACK? + tree.removeMouseListener(l); + } + + public void setSelectionMode(int selectionMode) { + tree.getSelectionModel().setSelectionMode(selectionMode); + } + + public int getSelectionMode() { + return tree.getSelectionModel().getSelectionMode(); + } + + protected R getItemsFromPaths(TreePath[] paths, + Collector collector) { + return Stream.of(paths) + .map(p -> (AbstractNode) p.getLastPathComponent()) + .collect(collector); + } + + protected AbstractNode getItemFromPath(TreePath path) { + if (path == null) { + return null; + } + return (AbstractNode) path.getLastPathComponent(); + } + + public List getSelectedItems() { + return getItemsFromPaths(tree.getSelectionPaths(), Collectors.toList()); + } + + public AbstractNode getSelectedItem() { + return getItemFromPath(tree.getSelectionPath()); + } + + public AbstractNode getNode(TraceObjectKeyPath path) { + return treeModel.getNode(path); + } + + public void setSelectedKeyPaths(Collection keyPaths) { + List nodes = new ArrayList<>(); + for (TraceObjectKeyPath path : keyPaths) { + AbstractNode node = getNode(path); + if (node != null) { + nodes.add(node); + } + } + tree.setSelectedNodes(nodes); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/PathTableModel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/PathTableModel.java new file mode 100644 index 0000000000..8e3e1ae898 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/PathTableModel.java @@ -0,0 +1,155 @@ +/* ### + * 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.debug.gui.model; + +import java.awt.Color; +import java.util.*; +import java.util.stream.Stream; + +import com.google.common.collect.Range; + +import docking.widgets.table.TableColumnDescriptor; +import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow; +import ghidra.app.plugin.core.debug.gui.model.columns.*; +import ghidra.framework.plugintool.Plugin; +import ghidra.trace.model.Trace; +import ghidra.trace.model.target.TraceObjectValPath; + +public class PathTableModel extends AbstractQueryTableModel { + /** Initialized in {@link #createTableColumnDescriptor()}, which precedes this. */ + private TracePathValueColumn valueColumn; + private TracePathLastLifespanPlotColumn lifespanPlotColumn; + + protected static Stream distinctKeyPath( + Stream stream) { + Set> seen = new HashSet<>(); + return stream.filter(path -> seen.add(path.getKeyList())); + } + + public class PathRow { + private final TraceObjectValPath path; + private final Object value; + + public PathRow(TraceObjectValPath path) { + this.path = path; + this.value = computeValue(); + } + + public TraceObjectValPath getPath() { + return path; + } + + public Object computeValue() { + // Spare fetching the root unless it's really needed + if (path.getLastEntry() == null) { + return getTrace().getObjectManager().getRootObject(); + } + return path.getDestinationValue(null); + } + + public Object getValue() { + return value; + } + + /** + * Get a non-HTML string representing how this row's value should be sorted, filtered, etc. + * + * @return the display string + */ + public String getDisplay() { + return display.getEdgeDisplay(path.getLastEntry()); + } + + /** + * Get an HTML string representing how this row's value should be displayed + * + * @return the display string + */ + public String getHtmlDisplay() { + return display.getEdgeHtmlDisplay(path.getLastEntry()); + } + + public String getToolTip() { + return display.getEdgeToolTip(path.getLastEntry()); + } + + public boolean isModified() { + return isValueModified(path.getLastEntry()); + } + } + + public PathTableModel(Plugin plugin) { + super("Attribute Model", plugin); + } + + protected void updateTimelineMax() { + Long max = getTrace() == null ? null : getTrace().getTimeManager().getMaxSnap(); + Range fullRange = Range.closed(0L, max == null ? 1 : max + 1); + lifespanPlotColumn.setFullRange(fullRange); + } + + @Override + protected void traceChanged() { + updateTimelineMax(); + super.traceChanged(); + } + + @Override + protected void showHiddenChanged() { + reload(); + super.showHiddenChanged(); + } + + @Override + protected void maxSnapChanged() { + updateTimelineMax(); + refresh(); + } + + protected static boolean isAnyHidden(TraceObjectValPath path) { + return path.getEntryList().stream().anyMatch(v -> v.isHidden()); + } + + @Override + protected Stream streamRows(Trace trace, ModelQuery query, Range span) { + // TODO: For queries with early wildcards, this is not efficient + // May need to incorporate filtering hidden into the query execution itself. + return distinctKeyPath(query.streamPaths(trace, span) + .filter(p -> isShowHidden() || !isAnyHidden(p))) + .map(PathRow::new); + } + + @Override + protected TableColumnDescriptor createTableColumnDescriptor() { + TableColumnDescriptor descriptor = new TableColumnDescriptor<>(); + descriptor.addHiddenColumn(new TracePathStringColumn()); + descriptor.addVisibleColumn(new TracePathLastKeyColumn()); + descriptor.addVisibleColumn(valueColumn = new TracePathValueColumn()); + descriptor.addVisibleColumn(new TracePathLastLifespanColumn()); + descriptor.addHiddenColumn(lifespanPlotColumn = new TracePathLastLifespanPlotColumn()); + return descriptor; + } + + @Override + public void setDiffColor(Color diffColor) { + valueColumn.setDiffColor(diffColor); + } + + @Override + public void setDiffColorSel(Color diffColorSel) { + valueColumn.setDiffColorSel(diffColorSel); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/PathsTablePanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/PathsTablePanel.java new file mode 100644 index 0000000000..f3d9cf4a3a --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/PathsTablePanel.java @@ -0,0 +1,30 @@ +/* ### + * 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.debug.gui.model; + +import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow; +import ghidra.framework.plugintool.Plugin; + +public class PathsTablePanel extends AbstractQueryTablePanel { + public PathsTablePanel(Plugin plugin) { + super(plugin); + } + + @Override + protected AbstractQueryTableModel createModel(Plugin plugin) { + return new PathTableModel(plugin); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastKeyColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastKeyColumn.java new file mode 100644 index 0000000000..55eee69287 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastKeyColumn.java @@ -0,0 +1,42 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.model.columns; + +import docking.widgets.table.AbstractDynamicTableColumn; +import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.trace.model.Trace; +import ghidra.trace.model.target.TraceObjectValPath; +import ghidra.trace.model.target.TraceObjectValue; + +public class TracePathLastKeyColumn extends AbstractDynamicTableColumn { + @Override + public String getColumnName() { + return "Key"; + } + + @Override + public String getValue(PathRow rowObject, Settings settings, Trace data, + ServiceProvider serviceProvider) throws IllegalArgumentException { + TraceObjectValPath path = rowObject.getPath(); + TraceObjectValue lastEntry = path.getLastEntry(); + if (lastEntry == null) { + return ""; + } + return lastEntry.getEntryKey(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastLifespanColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastLifespanColumn.java new file mode 100644 index 0000000000..d27a693786 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastLifespanColumn.java @@ -0,0 +1,44 @@ +/* ### + * 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.debug.gui.model.columns; + +import com.google.common.collect.Range; + +import docking.widgets.table.AbstractDynamicTableColumn; +import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.trace.model.Trace; +import ghidra.trace.model.target.TraceObjectValue; + +public class TracePathLastLifespanColumn + extends AbstractDynamicTableColumn, Trace> { + + @Override + public String getColumnName() { + return "Life"; + } + + @Override + public Range getValue(PathRow rowObject, Settings settings, Trace data, + ServiceProvider serviceProvider) throws IllegalArgumentException { + TraceObjectValue lastEntry = rowObject.getPath().getLastEntry(); + if (lastEntry == null) { + return Range.all(); + } + return lastEntry.getLifespan(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastLifespanPlotColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastLifespanPlotColumn.java new file mode 100644 index 0000000000..1be1443188 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastLifespanPlotColumn.java @@ -0,0 +1,60 @@ +/* ### + * 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.debug.gui.model.columns; + +import com.google.common.collect.Range; + +import docking.widgets.table.AbstractDynamicTableColumn; +import docking.widgets.table.RangeTableCellRenderer; +import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.trace.model.Trace; +import ghidra.trace.model.target.TraceObjectValue; +import ghidra.util.table.column.GColumnRenderer; + +public class TracePathLastLifespanPlotColumn + extends AbstractDynamicTableColumn, Trace> { + + private final RangeTableCellRenderer cellRenderer = new RangeTableCellRenderer<>(); + + @Override + public String getColumnName() { + return "Plot"; + } + + @Override + public Range getValue(PathRow rowObject, Settings settings, Trace data, + ServiceProvider serviceProvider) throws IllegalArgumentException { + TraceObjectValue lastEntry = rowObject.getPath().getLastEntry(); + if (lastEntry == null) { + return Range.all(); + } + return lastEntry.getLifespan(); + } + + @Override + public GColumnRenderer> getColumnRenderer() { + return cellRenderer; + } + + // TODO: header renderer + + public void setFullRange(Range fullRange) { + cellRenderer.setFullRange(fullRange); + // TODO: header, too + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathStringColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathStringColumn.java new file mode 100644 index 0000000000..93e6c6137f --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathStringColumn.java @@ -0,0 +1,36 @@ +/* ### + * 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.debug.gui.model.columns; + +import docking.widgets.table.AbstractDynamicTableColumn; +import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow; +import ghidra.dbg.util.PathUtils; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.trace.model.Trace; + +public class TracePathStringColumn extends AbstractDynamicTableColumn { + @Override + public String getColumnName() { + return "Path"; + } + + @Override + public String getValue(PathRow rowObject, Settings settings, Trace data, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return PathUtils.toString(rowObject.getPath().getKeyList()); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathValueColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathValueColumn.java new file mode 100644 index 0000000000..a65e8259ff --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathValueColumn.java @@ -0,0 +1,93 @@ +/* ### + * 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.debug.gui.model.columns; + +import java.awt.Color; +import java.awt.Component; + +import javax.swing.JTable; + +import docking.widgets.table.AbstractDynamicTableColumn; +import docking.widgets.table.GTableCellRenderingData; +import ghidra.app.plugin.core.debug.gui.DebuggerResources; +import ghidra.app.plugin.core.debug.gui.model.ColorsModified; +import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.trace.model.Trace; +import ghidra.util.table.column.AbstractGColumnRenderer; +import ghidra.util.table.column.GColumnRenderer; + +public class TracePathValueColumn extends AbstractDynamicTableColumn { + private final class ValueRenderer extends AbstractGColumnRenderer + implements ColorsModified.InTable { + { + setHTMLRenderingEnabled(true); + } + + @Override + public String getFilterString(PathRow t, Settings settings) { + return t.getDisplay(); + } + + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + super.getTableCellRendererComponent(data); + PathRow row = (PathRow) data.getValue(); + setText(row.getHtmlDisplay()); + setToolTipText(row.getToolTip()); + setForeground(getForegroundFor(data.getTable(), row.isModified(), data.isSelected())); + return this; + } + + @Override + public Color getDiffForeground(JTable table) { + return diffColor; + } + + @Override + public Color getDiffSelForeground(JTable table) { + return diffColorSel; + } + } + + private Color diffColor = DebuggerResources.DEFAULT_COLOR_VALUE_CHANGED; + private Color diffColorSel = DebuggerResources.DEFAULT_COLOR_VALUE_CHANGED_SEL; + + @Override + public String getColumnName() { + return "Value"; + } + + @Override + public PathRow getValue(PathRow rowObject, Settings settings, Trace data, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject; + } + + @Override + public GColumnRenderer getColumnRenderer() { + return new ValueRenderer(); + } + + public void setDiffColor(Color diffColor) { + this.diffColor = diffColor; + } + + public void setDiffColorSel(Color diffColorSel) { + this.diffColorSel = diffColorSel; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueKeyColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueKeyColumn.java new file mode 100644 index 0000000000..875e01f877 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueKeyColumn.java @@ -0,0 +1,35 @@ +/* ### + * 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.debug.gui.model.columns; + +import docking.widgets.table.AbstractDynamicTableColumn; +import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.trace.model.Trace; + +public class TraceValueKeyColumn extends AbstractDynamicTableColumn { + @Override + public String getColumnName() { + return "Key"; + } + + @Override + public String getValue(ValueRow rowObject, Settings settings, Trace data, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject.getKey(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueLifeColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueLifeColumn.java new file mode 100644 index 0000000000..f63a7d93aa --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueLifeColumn.java @@ -0,0 +1,39 @@ +/* ### + * 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.debug.gui.model.columns; + +import com.google.common.collect.RangeSet; + +import docking.widgets.table.AbstractDynamicTableColumn; +import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.trace.model.Trace; + +public class TraceValueLifeColumn + extends AbstractDynamicTableColumn, Trace> { + + @Override + public String getColumnName() { + return "Life"; + } + + @Override + public RangeSet getValue(ValueRow rowObject, Settings settings, Trace data, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject.getLife(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueLifePlotColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueLifePlotColumn.java new file mode 100644 index 0000000000..c7d408f18b --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueLifePlotColumn.java @@ -0,0 +1,56 @@ +/* ### + * 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.debug.gui.model.columns; + +import com.google.common.collect.Range; +import com.google.common.collect.RangeSet; + +import docking.widgets.table.AbstractDynamicTableColumn; +import docking.widgets.table.RangeSetTableCellRenderer; +import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.trace.model.Trace; +import ghidra.util.table.column.GColumnRenderer; + +public class TraceValueLifePlotColumn + extends AbstractDynamicTableColumn, Trace> { + + private final RangeSetTableCellRenderer cellRenderer = new RangeSetTableCellRenderer<>(); + + @Override + public String getColumnName() { + return "Plot"; + } + + @Override + public RangeSet getValue(ValueRow rowObject, Settings settings, Trace data, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject.getLife(); + } + + @Override + public GColumnRenderer> getColumnRenderer() { + return cellRenderer; + } + + // TODO: The header renderer + + public void setFullRange(Range fullRange) { + cellRenderer.setFullRange(fullRange); + // TODO: set header's full range, too + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueObjectAttributeColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueObjectAttributeColumn.java new file mode 100644 index 0000000000..dc313d38dc --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueObjectAttributeColumn.java @@ -0,0 +1,180 @@ +/* ### + * 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.debug.gui.model.columns; + +import java.awt.Color; +import java.awt.Component; +import java.util.Comparator; +import java.util.function.Function; + +import javax.swing.JTable; + +import docking.widgets.table.*; +import docking.widgets.table.sort.ColumnRenderedValueBackupComparator; +import docking.widgets.table.sort.DefaultColumnComparator; +import ghidra.app.plugin.core.debug.gui.DebuggerResources; +import ghidra.app.plugin.core.debug.gui.model.ColorsModified; +import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow; +import ghidra.dbg.target.TargetAttacher.TargetAttachKindSet; +import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.dbg.target.TargetMethod.TargetParameterMap; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.TargetSteppable.TargetStepKindSet; +import ghidra.dbg.target.schema.SchemaContext; +import ghidra.dbg.target.schema.TargetObjectSchema; +import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.trace.model.Trace; +import ghidra.trace.model.target.TraceObject; +import ghidra.trace.model.target.TraceObjectValue; +import ghidra.util.table.column.AbstractGColumnRenderer; +import ghidra.util.table.column.GColumnRenderer; + +public class TraceValueObjectAttributeColumn + extends AbstractDynamicTableColumn { + + public class AttributeRenderer extends AbstractGColumnRenderer + implements ColorsModified.InTable { + { + setHTMLRenderingEnabled(true); + } + + @Override + public String getFilterString(ValueRow t, Settings settings) { + return t.getAttributeDisplay(attributeName); + } + + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + super.getTableCellRendererComponent(data); + ValueRow row = (ValueRow) data.getValue(); + setText(row.getAttributeHtmlDisplay(attributeName)); + setToolTipText(row.getAttributeToolTip(attributeName)); + setForeground(getForegroundFor(data.getTable(), row.isAttributeModified(attributeName), + data.isSelected())); + return this; + } + + @Override + public Color getDiffForeground(JTable table) { + return diffColor; + } + + @Override + public Color getDiffSelForeground(JTable table) { + return diffColorSel; + } + } + + public static Class computeColumnType(SchemaContext ctx, AttributeSchema attributeSchema) { + TargetObjectSchema schema = ctx.getSchema(attributeSchema.getSchema()); + Class type = schema.getType(); + if (type == TargetObject.class) { + return TraceObject.class; + } + if (type == TargetExecutionState.class) { + return String.class; + } + if (type == TargetParameterMap.class) { + return String.class; + } + if (type == TargetAttachKindSet.class) { + return String.class; + } + if (type == TargetBreakpointKindSet.class) { + return String.class; + } + if (type == TargetStepKindSet.class) { + return String.class; + } + return type; + } + + public static TraceValueObjectAttributeColumn fromSchema(SchemaContext ctx, + AttributeSchema attributeSchema) { + String name = attributeSchema.getName(); + Class type = computeColumnType(ctx, attributeSchema); + return new TraceValueObjectAttributeColumn(name, type); + } + + private final String attributeName; + private final Class attributeType; + private final AttributeRenderer renderer = new AttributeRenderer(); + private final Comparator comparator; + + private Color diffColor = DebuggerResources.DEFAULT_COLOR_VALUE_CHANGED; + private Color diffColorSel = DebuggerResources.DEFAULT_COLOR_VALUE_CHANGED_SEL; + + public TraceValueObjectAttributeColumn(String attributeName, Class attributeType) { + this.attributeName = attributeName; + this.attributeType = attributeType; + this.comparator = newTypedComparator(); + } + + @Override + public String getColumnName() { + /** + * TODO: These are going to have "_"-prefixed things.... Sure, they're "hidden", but if we + * remove them, we're going to hide important info. I'd like a way in the schema to specify + * which "interface attribute" an attribute satisfies. That way, the name can be + * human-friendly, but the interface can still find what it needs. + */ + return attributeName; + } + + @Override + public ValueRow getValue(ValueRow rowObject, Settings settings, Trace data, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject; + } + + @Override + public GColumnRenderer getColumnRenderer() { + return renderer; + } + + @Override + public Comparator getComparator(DynamicColumnTableModel model, int columnIndex) { + return comparator == null ? null + : comparator.thenComparing( + new ColumnRenderedValueBackupComparator<>(model, columnIndex)); + } + + protected Object getAttributeValue(ValueRow row) { + TraceObjectValue edge = row.getAttribute(attributeName); + return edge == null ? null : edge.getValue(); + } + + protected > Comparator newTypedComparator() { + if (Comparable.class.isAssignableFrom(attributeType)) { + @SuppressWarnings("unchecked") + Class cls = (Class) attributeType.asSubclass(Comparable.class); + Function keyExtractor = r -> cls.cast(getAttributeValue(r)); + return Comparator.comparing(keyExtractor, new DefaultColumnComparator()); + } + return null; // Opt for the default filter-string-based comparator + } + + public void setDiffColor(Color diffColor) { + this.diffColor = diffColor; + } + + public void setDiffColorSel(Color diffColorSel) { + this.diffColorSel = diffColorSel; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueValColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueValColumn.java new file mode 100644 index 0000000000..c3c0ad88fb --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueValColumn.java @@ -0,0 +1,116 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.model.columns; + +import java.awt.Color; +import java.awt.Component; +import java.util.Comparator; + +import javax.swing.JTable; + +import docking.widgets.table.*; +import docking.widgets.table.sort.ColumnRenderedValueBackupComparator; +import ghidra.app.plugin.core.debug.gui.DebuggerResources; +import ghidra.app.plugin.core.debug.gui.model.ColorsModified; +import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.trace.model.Trace; +import ghidra.util.table.column.AbstractGColumnRenderer; +import ghidra.util.table.column.GColumnRenderer; + +public class TraceValueValColumn extends AbstractDynamicTableColumn { + private final class ValRenderer extends AbstractGColumnRenderer + implements ColorsModified.InTable { + { + setHTMLRenderingEnabled(true); + } + + @Override + public String getFilterString(ValueRow t, Settings settings) { + return t.getDisplay(); + } + + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + super.getTableCellRendererComponent(data); + ValueRow row = (ValueRow) data.getValue(); + setText(row.getHtmlDisplay()); + setToolTipText(row.getToolTip()); + setForeground(getForegroundFor(data.getTable(), row.isModified(), data.isSelected())); + return this; + } + + @Override + public Color getDiffForeground(JTable table) { + return diffColor; + } + + @Override + public Color getDiffSelForeground(JTable table) { + return diffColorSel; + } + } + + private Color diffColor = DebuggerResources.DEFAULT_COLOR_VALUE_CHANGED; + private Color diffColorSel = DebuggerResources.DEFAULT_COLOR_VALUE_CHANGED_SEL; + private final ValRenderer renderer = new ValRenderer(); + + @Override + public String getColumnName() { + return "Value"; + } + + @Override + public ValueRow getValue(ValueRow rowObject, Settings settings, Trace data, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject; + } + + @Override + public GColumnRenderer getColumnRenderer() { + return renderer; + } + + @Override + public Comparator getComparator(DynamicColumnTableModel model, int columnIndex) { + return getComparator() + .thenComparing(new ColumnRenderedValueBackupComparator<>(model, columnIndex)); + } + + @Override + @SuppressWarnings("unchecked") + public Comparator getComparator() { + return (r1, r2) -> { + Object v1 = r1.getValue().getValue(); + Object v2 = r2.getValue().getValue(); + if (v1 instanceof Comparable) { + if (v1.getClass() == v2.getClass()) { + return ((Comparable) v1).compareTo(v2); + } + } + return 0; // Defer to backup comparator + }; + } + + public void setDiffColor(Color diffColor) { + this.diffColor = diffColor; + } + + public void setDiffColorSel(Color diffColorSel) { + this.diffColorSel = diffColorSel; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProvider.java index 7ae54dabd0..5ba13f399b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProvider.java @@ -608,9 +608,9 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter .onAction(c -> selectRegistersActivated()) .buildAndInstallLocal(this); if (!isClone) { - actionCreateSnapshot = DebuggerResources.CreateSnapshotAction.builder(plugin) + actionCreateSnapshot = DebuggerResources.CloneWindowAction.builder(plugin) .enabledWhen(c -> current.getThread() != null) - .onAction(c -> createSnapshotActivated()) + .onAction(c -> cloneWindowActivated()) .buildAndInstallLocal(this); } actionEnableEdits = DebuggerResources.EnableEditsAction.builder(plugin) @@ -639,7 +639,7 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter tool.showDialog(availableRegsDialog); } - private void createSnapshotActivated() { + private void cloneWindowActivated() { DebuggerRegistersProvider clone = cloneAsDisconnected(); clone.setIntraGroupPosition(WindowPosition.RIGHT); tool.showComponentProvider(clone, true); @@ -961,8 +961,14 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter public static LinkedHashSet collectCommonRegisters(CompilerSpec cSpec) { Language lang = cSpec.getLanguage(); LinkedHashSet result = new LinkedHashSet<>(); - result.add(cSpec.getStackPointer()); - result.add(lang.getProgramCounter()); + Register sp = cSpec.getStackPointer(); + if (sp != null) { + result.add(sp); + } + Register pc = lang.getProgramCounter(); + if (pc != null) { + result.add(pc); + } for (Register reg : lang.getRegisters()) { //if (reg.getGroup() != null) { // continue; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorder.java index 3b4e06415f..d307a50da0 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorder.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorder.java @@ -392,6 +392,9 @@ public class ObjectBasedTraceRecorder implements TraceRecorder { @Override public TargetThread getTargetThread(TraceThread thread) { + if (thread == null) { + return null; + } return objectRecorder.getTargetInterface(thread, TraceObjectThread.class, TargetThread.class); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/ObjectRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/ObjectRecorder.java index 9f37ec4227..f93e634099 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/ObjectRecorder.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/record/ObjectRecorder.java @@ -23,6 +23,7 @@ import org.apache.commons.collections4.bidimap.DualHashBidiMap; import com.google.common.collect.Range; +import ghidra.dbg.DebuggerObjectModel; import ghidra.dbg.target.TargetAttacher.TargetAttachKind; import ghidra.dbg.target.TargetAttacher.TargetAttachKindSet; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; @@ -75,6 +76,23 @@ class ObjectRecorder { return targetObject == null ? null : targetObject.obj; } + /** + * List the names of interfaces on the object not already covered by the schema + * + * @param object the object + * @return the comma-separated list of interface names + */ + protected String computeExtraInterfaces(TargetObject object) { + Set result = new LinkedHashSet<>(object.getInterfaceNames()); + for (Class iface : object.getSchema().getInterfaces()) { + result.remove(DebuggerObjectModel.requireIfaceName(iface)); + } + if (result.isEmpty()) { + return null; + } + return result.stream().collect(Collectors.joining(",")); + } + protected void recordCreated(long snap, TargetObject object) { TraceObject traceObject; if (object.isRoot()) { @@ -91,6 +109,10 @@ class ObjectRecorder { Msg.error(this, "Received created for an object that already exists: " + exists); } } + String extras = computeExtraInterfaces(object); + // Note: null extras will erase previous value, if necessary. + traceObject.setAttribute(Range.atLeast(snap), + TraceObject.EXTRA_INTERFACES_ATTRIBUTE_NAME, extras); } protected void recordInvalidated(long snap, TargetObject object) { diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProviderTest.java new file mode 100644 index 0000000000..52cb96ac2d --- /dev/null +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProviderTest.java @@ -0,0 +1,766 @@ +/* ### + * 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.debug.gui.model; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.Set; + +import org.jdom.JDOMException; +import org.junit.*; + +import com.google.common.collect.Range; + +import docking.widgets.table.DynamicTableColumn; +import docking.widgets.table.GDynamicColumnTableModel; +import generic.Unique; +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; +import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.PrimitiveRow; +import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow; +import ghidra.app.plugin.core.debug.gui.model.ObjectTreeModel.AbstractNode; +import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow; +import ghidra.app.plugin.core.debug.gui.model.columns.TraceValueValColumn; +import ghidra.dbg.target.TargetEventScope; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.schema.SchemaContext; +import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName; +import ghidra.dbg.target.schema.XmlSchemaContext; +import ghidra.trace.database.target.DBTraceObject; +import ghidra.trace.database.target.DBTraceObjectManager; +import ghidra.trace.model.target.*; +import ghidra.trace.model.target.TraceObject.ConflictResolution; +import ghidra.util.database.UndoableTransaction; + +public class DebuggerModelProviderTest extends AbstractGhidraHeadedDebuggerGUITest { + + protected static final SchemaContext CTX; + + static { + try { + CTX = XmlSchemaContext.deserialize("" + // + "" + // + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + ""); + } + catch (JDOMException e) { + throw new AssertionError(); + } + } + + protected static Integer findColumnOfClass(GDynamicColumnTableModel model, + Class> cls) { + for (int i = 0; i < model.getColumnCount(); i++) { + DynamicTableColumn column = model.getColumn(i); + if (cls.isAssignableFrom(column.getClass())) { + return i; + } + } + return null; + } + + protected DebuggerModelPlugin modelPlugin; + protected DebuggerModelProvider modelProvider; + + @Before + public void setUpModelProviderTest() throws Exception { + modelPlugin = addPlugin(tool, DebuggerModelPlugin.class); + modelProvider = waitForComponentProvider(DebuggerModelProvider.class); + + // So I can manipulate the coordinates + //addPlugin(tool, DebuggerThreadsPlugin.class); + } + + @After + public void tearDownModelProviderTest() throws Exception { + traceManager.activate(DebuggerCoordinates.NOWHERE); + waitForSwing(); + waitForCondition(() -> !modelProvider.objectsTreePanel.tree.isBusy()); + waitForCondition(() -> !modelProvider.elementsTablePanel.tableModel.isBusy()); + waitForCondition(() -> !modelProvider.attributesTablePanel.tableModel.isBusy()); + runSwing(() -> traceManager.closeAllTraces()); + } + + protected void populateSnapshots() throws Throwable { + try (UndoableTransaction tid = tb.startTransaction()) { + tb.trace.getTimeManager().getSnapshot(20, true); + } + } + + protected TraceObjectValue createSessionObject() throws Throwable { + DBTraceObjectManager objects = tb.trace.getObjectManager(); + try (UndoableTransaction tid = tb.startTransaction()) { + return objects.createRootObject(CTX.getSchema(new SchemaName("Session"))); + } + } + + protected DBTraceObject createThread(long i, DBTraceObject prevThread) { + DBTraceObjectManager objects = tb.trace.getObjectManager(); + TraceObjectKeyPath threadContainerPath = TraceObjectKeyPath.parse("Processes[0].Threads"); + DBTraceObject thread = objects.createObject(threadContainerPath.index(i)); + thread.insert(Range.closed(i, 10L), ConflictResolution.DENY); + thread.insert(Range.atLeast(10 + i), ConflictResolution.DENY); + thread.setAttribute(Range.atLeast(i), "Attribute " + i, "Some value"); + thread.setAttribute(Range.atLeast(i), "_display", "Thread " + i); + thread.setAttribute(Range.atLeast(i), "_self", thread); + if (prevThread != null) { + thread.setAttribute(Range.atLeast(i), "_prev", prevThread); + prevThread.setAttribute(Range.atLeast(i), "_next", thread); + } + objects.getRootObject() + .setAttribute(Range.atLeast(i), TargetEventScope.EVENT_OBJECT_ATTRIBUTE_NAME, + thread); + return thread; + } + + protected void populateThreads() throws Throwable { + try (UndoableTransaction tid = tb.startTransaction()) { + DBTraceObject prevThread = null; + for (long i = 0; i < 10; i++) { + DBTraceObject thread = createThread(i, prevThread); + prevThread = thread; + } + } + } + + protected void addThread10() throws Throwable { + DBTraceObjectManager objects = tb.trace.getObjectManager(); + try (UndoableTransaction tid = tb.startTransaction()) { + createThread(10, objects.getObjectByCanonicalPath( + TraceObjectKeyPath.parse("Processes[0].Threads[9]"))); + } + } + + protected void populateHandles() throws Throwable { + DBTraceObjectManager objects = tb.trace.getObjectManager(); + try (UndoableTransaction tid = tb.startTransaction()) { + DBTraceObject handleContainer = + objects.createObject(TraceObjectKeyPath.parse("Processes[0].Handles")); + handleContainer.insert(Range.atLeast(0L), ConflictResolution.DENY); + for (int i = 0; i < 10; i++) { + handleContainer.setElement(Range.atLeast((long) -i), i, + (i * 0xdeadbeef) % 0xbadc0de); + } + } + } + + protected void populateLinks() throws Throwable { + DBTraceObjectManager objects = tb.trace.getObjectManager(); + TraceObjectKeyPath threadContainerPath = TraceObjectKeyPath.parse("Processes[0].Threads"); + try (UndoableTransaction tid = tb.startTransaction()) { + DBTraceObject linkContainer = + objects.createObject(TraceObjectKeyPath.parse("Processes[0].Links")); + linkContainer.insert(Range.atLeast(0L), ConflictResolution.DENY); + for (int i = 0; i < 10; i++) { + linkContainer.setElement(Range.atLeast(0L), i, + objects.getObjectByCanonicalPath(threadContainerPath.index(9 - i))); + } + } + } + + protected void populateBoxedPrimitive() throws Throwable { + DBTraceObjectManager objects = tb.trace.getObjectManager(); + try (UndoableTransaction tid = tb.startTransaction()) { + TraceObject boxed = + objects.createObject(TraceObjectKeyPath.parse("Processes[0].Boxed")); + boxed.insert(Range.atLeast(0L), ConflictResolution.DENY); + boxed.setAttribute(Range.atLeast(2L), TargetObject.DISPLAY_ATTRIBUTE_NAME, "2"); + boxed.setAttribute(Range.atLeast(4L), TargetObject.DISPLAY_ATTRIBUTE_NAME, "4"); + } + } + + protected void createTraceAndPopulateObjects() throws Throwable { + createTrace(); + populateSnapshots(); + createSessionObject(); + populateThreads(); + populateHandles(); + populateLinks(); + populateBoxedPrimitive(); + } + + protected void assertPathIs(TraceObjectKeyPath path, int elemCount, int attrCount) { + assertEquals(path, modelProvider.getPath()); + assertEquals(path.toString(), modelProvider.pathField.getText()); + AbstractNode item = modelProvider.objectsTreePanel.getSelectedItem(); + assertNotNull(item); + assertEquals(path, item.getValue().getChild().getCanonicalPath()); + // Table model is threaded + waitForPass(() -> assertEquals(elemCount, + modelProvider.elementsTablePanel.tableModel.getModelData().size())); + waitForPass(() -> assertEquals(attrCount, + modelProvider.attributesTablePanel.tableModel.getModelData().size())); + } + + protected void assertPathIsThreadsContainer() { + assertPathIs(TraceObjectKeyPath.parse("Processes[0].Threads"), 10, 0); + } + + @Test + public void testSetPathWOutTrace() throws Throwable { + modelProvider.setPath(TraceObjectKeyPath.parse("")); + waitForSwing(); + modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Threads")); + waitForSwing(); + modelProvider.setPath(TraceObjectKeyPath.parse("")); + waitForSwing(); + } + + @Test + public void testSelectRootWOutTrace() throws Throwable { + modelProvider.objectsTreePanel.setSelectedKeyPaths(Set.of(TraceObjectKeyPath.parse(""))); + waitForSwing(); + } + + @Test + public void testSelectRootWOutObjects() throws Throwable { + createTrace(); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + + modelProvider.objectsTreePanel.setSelectedKeyPaths(Set.of(TraceObjectKeyPath.parse(""))); + waitForSwing(); + } + + @Test + public void testSetPathApi() throws Throwable { + createTraceAndPopulateObjects(); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Threads")); + waitForSwing(); + + assertPathIsThreadsContainer(); + } + + @Test + public void testSetPathViaField() throws Throwable { + createTraceAndPopulateObjects(); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + modelProvider.pathField.setText("Processes[0].Threads"); + modelProvider.pathField.getInputVerifier().verify(modelProvider.pathField); + waitForSwing(); + + assertPathIsThreadsContainer(); + } + + @Test + public void testSetPathViaTree() throws Throwable { + createTraceAndPopulateObjects(); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + modelProvider.objectsTreePanel + .setSelectedKeyPaths(List.of(TraceObjectKeyPath.parse("Processes[0].Threads"))); + waitForSwing(); + + waitForPass(() -> assertPathIsThreadsContainer()); + } + + @Test + public void testSelectElementDisplaysAttributes() throws Throwable { + createTraceAndPopulateObjects(); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Threads")); + waitForSwing(); + + ValueRow selElem = waitForValue(() -> { + List rows = modelProvider.elementsTablePanel.tableModel.getModelData(); + if (rows.size() != 10) { + return null; + } + return rows.get(2); + }); + modelProvider.elementsTablePanel.setSelectedItem(selElem); + waitForSwing(); + + waitForPass(() -> assertEquals(3, + modelProvider.attributesTablePanel.tableModel.getModelData().size())); + } + + @Test + public void testSetPathNoExist() throws Throwable { + createTraceAndPopulateObjects(); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].NoSuch")); + waitForSwing(); + + assertEquals("No such object at path Processes[0].NoSuch", tool.getStatusInfo()); + } + + @Test + public void testPrimitiveElements() throws Throwable { + createTraceAndPopulateObjects(); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Handles")); + waitForSwing(); + + int valColIndex = + waitForValue(() -> findColumnOfClass(modelProvider.elementsTablePanel.tableModel, + TraceValueValColumn.class)); + + waitForPass(() -> { + for (int i = 0; i < 10; i++) { + Object obj = modelProvider.elementsTablePanel.tableModel.getValueAt(i, valColIndex); + assertTrue(obj instanceof PrimitiveRow); + PrimitiveRow row = (PrimitiveRow) obj; + assertEquals(Integer.toString((0xdeadbeef * i) % 0xbadc0de), row.getDisplay()); + } + }); + } + + @Test + public void testCancelEditPath() throws Throwable { + createTraceAndPopulateObjects(); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Threads")); + waitForSwing(); + + modelProvider.pathField.setText("SomeNonsenseToBeCancelled"); + triggerEscapeKey(modelProvider.pathField); + waitForSwing(); + + assertPathIsThreadsContainer(); + } + + @Test + public void testDoubleClickLinkInElementsTable() throws Throwable { + createTraceAndPopulateObjects(); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Links")); + waitForSwing(); + + ValueRow row2 = waitForValue(() -> { + return modelProvider.elementsTablePanel.tableModel.getModelData() + .stream() + .filter(r -> r.getValue().getEntryKey().equals("[2]")) + .findAny() + .orElse(null); + }); + modelProvider.elementsTablePanel.setSelectedItem(row2); + waitForSwing(); + int rowIndex = waitForValue(() -> { + int index = modelProvider.elementsTablePanel.table.getSelectedRow(); + if (index == -1) { + return null; + } + return index; + }); + clickTableCell(modelProvider.elementsTablePanel.table, rowIndex, 0, 2); + + assertPathIs(TraceObjectKeyPath.parse("Processes[0].Threads[7]"), 0, 3); + } + + @Test + public void testDoubleClickObjectInElementsTable() throws Throwable { + createTraceAndPopulateObjects(); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Threads")); + waitForSwing(); + + ValueRow row2 = waitForValue(() -> { + return modelProvider.elementsTablePanel.tableModel.getModelData() + .stream() + .filter(r -> r.getValue().getEntryKey().equals("[2]")) + .findAny() + .orElse(null); + }); + modelProvider.elementsTablePanel.setSelectedItem(row2); + waitForSwing(); + int rowIndex = waitForValue(() -> { + int index = modelProvider.elementsTablePanel.table.getSelectedRow(); + if (index == -1) { + return null; + } + return index; + }); + clickTableCell(modelProvider.elementsTablePanel.table, rowIndex, 0, 2); + + assertPathIs(TraceObjectKeyPath.parse("Processes[0].Threads[2]"), 0, 3); + } + + protected void selectAttribute(String key) { + PathRow rowNext = waitForValue(() -> { + return modelProvider.attributesTablePanel.tableModel.getModelData() + .stream() + .filter(r -> { + TraceObjectValue last = r.getPath().getLastEntry(); + if (last == null) { + return false; + } + return last.getEntryKey().equals(key); + }) + .findAny() + .orElse(null); + }); + modelProvider.attributesTablePanel.setSelectedItem(rowNext); + } + + @Test + public void testDoubleClickLinkInAttributesTable() throws Throwable { + modelProvider.setShowHidden(true); + createTraceAndPopulateObjects(); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Threads[2]")); + waitForSwing(); + selectAttribute("_next"); + waitForSwing(); + + int rowIndex = waitForValue(() -> { + int index = modelProvider.attributesTablePanel.table.getSelectedRow(); + if (index == -1) { + return null; + } + return index; + }); + clickTableCell(modelProvider.attributesTablePanel.table, rowIndex, 0, 2); + + assertPathIs(TraceObjectKeyPath.parse("Processes[0].Threads[3]"), 0, 5); + } + + @Test + public void testDoubleClickObjectInAttributesTable() throws Throwable { + createTraceAndPopulateObjects(); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0]")); + waitForSwing(); + + PathRow rowNext = waitForValue(() -> { + return modelProvider.attributesTablePanel.tableModel.getModelData() + .stream() + .filter(r -> { + TraceObjectValue last = r.getPath().getLastEntry(); + if (last == null) { + return false; + } + return last.getEntryKey().equals("Threads"); + }) + .findAny() + .orElse(null); + }); + modelProvider.attributesTablePanel.setSelectedItem(rowNext); + waitForSwing(); + int rowIndex = waitForValue(() -> { + int index = modelProvider.attributesTablePanel.table.getSelectedRow(); + if (index == -1) { + return null; + } + return index; + }); + clickTableCell(modelProvider.attributesTablePanel.table, rowIndex, 0, 2); + + assertPathIsThreadsContainer(); + } + + @Test + public void testActionLimitToSnap() throws Throwable { + assertFalse(modelProvider.isLimitToCurrentSnap()); + assertFalse(modelProvider.actionLimitToCurrentSnap.isSelected()); + createTraceAndPopulateObjects(); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Threads")); + waitForSwing(); + + assertPathIs(TraceObjectKeyPath.parse("Processes[0].Threads"), 10, 0); + + performAction(modelProvider.actionLimitToCurrentSnap); + assertTrue(modelProvider.isLimitToCurrentSnap()); + assertTrue(modelProvider.actionLimitToCurrentSnap.isSelected()); + assertPathIs(TraceObjectKeyPath.parse("Processes[0].Threads"), 1, 0); + + traceManager.activateSnap(5); + assertPathIs(TraceObjectKeyPath.parse("Processes[0].Threads"), 6, 0); + + performAction(modelProvider.actionLimitToCurrentSnap); + assertFalse(modelProvider.isLimitToCurrentSnap()); + assertFalse(modelProvider.actionLimitToCurrentSnap.isSelected()); + assertPathIs(TraceObjectKeyPath.parse("Processes[0].Threads"), 10, 0); + } + + @Test + public void testActionShowPrimitivesInTree() throws Throwable { + createTraceAndPopulateObjects(); + assertFalse(modelProvider.isShowPrimitivesInTree()); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Threads[2]")); + waitForSwing(); + + AbstractNode nodeThread2 = modelProvider.objectsTreePanel.getSelectedItem(); + assertEquals(1, nodeThread2.getChildren().size()); + + performAction(modelProvider.actionShowPrimitivesInTree, modelProvider, true); + assertTrue(modelProvider.isShowPrimitivesInTree()); + assertEquals(3, nodeThread2.getChildren().size()); + assertEquals(nodeThread2, modelProvider.objectsTreePanel.getSelectedItem()); + + performAction(modelProvider.actionShowPrimitivesInTree, modelProvider, true); + assertFalse(modelProvider.isShowPrimitivesInTree()); + assertEquals(1, nodeThread2.getChildren().size()); + assertEquals(nodeThread2, modelProvider.objectsTreePanel.getSelectedItem()); + } + + @Test + public void testActionFollowLink() throws Throwable { + modelProvider.setShowHidden(true); + assertDisabled(modelProvider, modelProvider.actionFollowLink); + createTraceAndPopulateObjects(); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Threads[2]")); + waitForSwing(); + selectAttribute("_next"); + waitForSwing(); + + assertEnabled(modelProvider, modelProvider.actionFollowLink); + performAction(modelProvider.actionFollowLink, modelProvider, true); + + assertPathIs(TraceObjectKeyPath.parse("Processes[0].Threads[3]"), 0, 5); + } + + @Test + public void testActionCloneWindow() throws Throwable { + createTraceAndPopulateObjects(); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Threads[2]")); + waitForSwing(); + + performAction(modelProvider.actionCloneWindow); + + DebuggerModelProvider clone = Unique.assertOne(modelPlugin.getDisconnectedProviders()); + + assertEquals(tb.trace, clone.current.getTrace()); + assertEquals(TraceObjectKeyPath.parse("Processes[0].Threads[2]"), clone.path); + } + + @Test + public void testPanesTrackAddElement() throws Throwable { + createTraceAndPopulateObjects(); + TraceObjectKeyPath path = TraceObjectKeyPath.parse("Processes[0].Threads"); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + modelProvider.setPath(path); + waitForSwing(); + + assertPathIsThreadsContainer(); + + addThread10(); + waitForSwing(); + + assertPathIs(path, 11, 0); + } + + @Test + public void testPanesTrackAddAttribute() throws Throwable { + createTraceAndPopulateObjects(); + TraceObjectKeyPath path = TraceObjectKeyPath.parse("Processes[0].Threads[2]"); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + modelProvider.setPath(path); + waitForSwing(); + + assertPathIs(path, 0, 3); + + try (UndoableTransaction tid = tb.startTransaction()) { + DBTraceObject thread = tb.trace.getObjectManager().getObjectByCanonicalPath(path); + thread.setAttribute(Range.atLeast(0L), "NewAttribute", 11); + } + waitForSwing(); + + assertPathIs(path, 0, 4); + } + + @Test + public void testPanesTrackRemoveElement() throws Throwable { + createTraceAndPopulateObjects(); + TraceObjectKeyPath path = TraceObjectKeyPath.parse("Processes[0].Threads"); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + modelProvider.setPath(path); + waitForSwing(); + + assertPathIsThreadsContainer(); + + try (UndoableTransaction tid = tb.startTransaction()) { + DBTraceObject threads = tb.trace.getObjectManager().getObjectByCanonicalPath(path); + threads.setElement(Range.all(), 2, null); + } + waitForSwing(); + + assertPathIs(path, 9, 0); + } + + @Test + public void testPanesTrackRemoveAttribute() throws Throwable { + createTraceAndPopulateObjects(); + TraceObjectKeyPath path = TraceObjectKeyPath.parse("Processes[0].Threads[2]"); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + modelProvider.setPath(path); + waitForSwing(); + + assertPathIs(path, 0, 3); + + try (UndoableTransaction tid = tb.startTransaction()) { + DBTraceObject thread = tb.trace.getObjectManager().getObjectByCanonicalPath(path); + thread.setAttribute(Range.all(), "_self", null); + } + waitForSwing(); + + assertPathIs(path, 0, 2); + } + + @Test + public void testPanesTrackLifespanChangedElement() throws Throwable { + modelProvider.setLimitToCurrentSnap(true); + createTraceAndPopulateObjects(); + TraceObjectKeyPath path = TraceObjectKeyPath.parse("Processes[0].Threads"); + TraceObject threads = tb.trace.getObjectManager().getObjectByCanonicalPath(path); + TraceObjectValue element2 = threads.getElement(2, 2); + + traceManager.activateTrace(tb.trace); + traceManager.activateSnap(2); + waitForSwing(); + modelProvider.setPath(path); + waitForSwing(); + + assertPathIs(path, 3, 0); + + try (UndoableTransaction tid = tb.startTransaction()) { + element2.setLifespan(Range.atLeast(10L), ConflictResolution.DENY); + } + waitForSwing(); + + assertPathIs(path, 2, 0); + + try (UndoableTransaction tid = tb.startTransaction()) { + element2.setLifespan(Range.atLeast(2L), ConflictResolution.DENY); + } + waitForSwing(); + + assertPathIs(path, 3, 0); + } + + @Test + public void testPanesTrackLifespanChangedAttribute() throws Throwable { + modelProvider.setLimitToCurrentSnap(true); + modelProvider.setShowHidden(true); + createTraceAndPopulateObjects(); + TraceObjectKeyPath path = TraceObjectKeyPath.parse("Processes[0].Threads[2]"); + TraceObject thread = tb.trace.getObjectManager().getObjectByCanonicalPath(path); + TraceObjectValue attrSelf = thread.getAttribute(2, "_self"); + + traceManager.activateTrace(tb.trace); + traceManager.activateSnap(2); + waitForSwing(); + modelProvider.setPath(path); + waitForSwing(); + + assertPathIs(path, 0, 4); // _next created at snap 3 + + try (UndoableTransaction tid = tb.startTransaction()) { + attrSelf.setLifespan(Range.atLeast(10L), ConflictResolution.DENY); + } + waitForSwing(); + + assertPathIs(path, 0, 3); + + try (UndoableTransaction tid = tb.startTransaction()) { + attrSelf.setLifespan(Range.atLeast(2L), ConflictResolution.DENY); + } + waitForSwing(); + + assertPathIs(path, 0, 4); + } + + @Test + public void testTreeTracksDisplayChange() throws Throwable { + createTraceAndPopulateObjects(); + TraceObjectKeyPath path = TraceObjectKeyPath.parse("Processes[0].Threads[2]"); + TraceObject thread = tb.trace.getObjectManager().getObjectByCanonicalPath(path); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + modelProvider.setPath(path); + waitForSwing(); + + AbstractNode node = + waitForValue(() -> modelProvider.objectsTreePanel.treeModel.getNode(path)); + assertEquals("[2]", node.getDisplayText()); + + try (UndoableTransaction tid = tb.startTransaction()) { + thread.setAttribute(Range.atLeast(0L), "_display", "Renamed Thread"); + } + waitForSwing(); + + waitForPass(() -> assertEquals("Renamed Thread", node.getDisplayText())); + } +} diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/ModelQueryTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/ModelQueryTest.java new file mode 100644 index 0000000000..8b349c8f48 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/ModelQueryTest.java @@ -0,0 +1,61 @@ +/* ### + * 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.debug.gui.model; + +import static ghidra.app.plugin.core.debug.gui.model.DebuggerModelProviderTest.CTX; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import com.google.common.collect.Range; + +import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; +import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName; +import ghidra.trace.database.target.DBTraceObjectManager; +import ghidra.trace.model.target.TraceObject.ConflictResolution; +import ghidra.trace.model.target.TraceObjectKeyPath; +import ghidra.trace.model.target.TraceObjectValue; +import ghidra.util.database.UndoableTransaction; + +public class ModelQueryTest extends AbstractGhidraHeadedDebuggerGUITest { + @Test + public void testIncludes() throws Throwable { + createTrace(); + + ModelQuery rootQuery = ModelQuery.parse(""); + ModelQuery threadQuery = ModelQuery.parse("Processes[].Threads[]"); + + try (UndoableTransaction tid = UndoableTransaction.start(tb.trace, "Init", true)) { + DBTraceObjectManager objects = tb.trace.getObjectManager(); + + TraceObjectValue rootVal = + objects.createRootObject(CTX.getSchema(new SchemaName("Session"))); + + TraceObjectValue thread0Val = + objects.createObject(TraceObjectKeyPath.parse("Processes[0].Threads[0]")) + .insert(Range.atLeast(0L), ConflictResolution.DENY) + .getLastEntry(); + + assertTrue(rootQuery.includes(Range.all(), rootVal)); + assertFalse(rootQuery.includes(Range.all(), thread0Val)); + + assertFalse(threadQuery.includes(Range.all(), rootVal)); + assertTrue(threadQuery.includes(Range.all(), thread0Val)); + assertFalse(threadQuery.includes(Range.lessThan(0L), thread0Val)); + } + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java index be3fc48e2b..8b97c8fd5c 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java @@ -414,7 +414,8 @@ public interface TargetObjectSchema { * *

* If this is the schema of the root object, then this gives the schema of the object at the - * given path in the model. + * given path in the model. This will always give a non-null result, though that result might be + * {@link EnumerableTargetObjectSchema#VOID}. * * @param path the relative path from an object having this schema to the desired successor * @return the schema for the successor diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/AllPathsMatcher.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/AllPathsMatcher.java deleted file mode 100644 index a8fa825b64..0000000000 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/AllPathsMatcher.java +++ /dev/null @@ -1,81 +0,0 @@ -/* ### - * 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.dbg.util; - -import java.util.List; -import java.util.Set; - -public enum AllPathsMatcher implements PathPredicates { - INSTANCE; - - @Override - public PathPredicates or(PathPredicates that) { - return this; - } - - @Override - public boolean matches(List path) { - return true; - } - - @Override - public boolean successorCouldMatch(List path, boolean strict) { - return true; - } - - @Override - public boolean ancestorMatches(List path, boolean strict) { - if (path.isEmpty() && strict) { - return false; - } - return true; - } - - @Override - public Set getNextKeys(List path) { - return Set.of("", "[]"); - } - - @Override - public Set getNextNames(List path) { - return Set.of(""); - } - - @Override - public Set getNextIndices(List path) { - return Set.of(""); - } - - @Override - public List getSingletonPath() { - return null; - } - - @Override - public PathPattern getSingletonPattern() { - return null; - } - - @Override - public PathPredicates applyKeys(List keys) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isEmpty() { - return false; - } -} diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathMatcher.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathMatcher.java index a12854de21..abcc33a7d1 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathMatcher.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathMatcher.java @@ -38,6 +38,21 @@ public class PathMatcher implements PathPredicates { return String.format("", StringUtils.join(patterns, "\n ")); } + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof PathMatcher)) { + return false; + } + PathMatcher that = (PathMatcher) obj; + if (!Objects.equals(this.patterns, that.patterns)) { + return false; + } + return true; + } + @Override public PathPredicates or(PathPredicates that) { PathMatcher result = new PathMatcher(); @@ -82,6 +97,11 @@ public class PathMatcher implements PathPredicates { return anyPattern(p -> p.ancestorMatches(path, strict)); } + @Override + public boolean ancestorCouldMatchRight(List path, boolean strict) { + return anyPattern(p -> p.ancestorCouldMatchRight(path, strict)); + } + @Override public List getSingletonPath() { if (patterns.size() != 1) { @@ -98,12 +118,7 @@ public class PathMatcher implements PathPredicates { return patterns.iterator().next(); } - @Override - public Set getNextKeys(List path) { - Set result = new HashSet<>(); - for (PathPattern pattern : patterns) { - result.addAll(pattern.getNextKeys(path)); - } + protected void coalesceWilds(Set result) { if (result.contains("")) { result.removeIf(PathUtils::isName); result.add(""); @@ -112,6 +127,15 @@ public class PathMatcher implements PathPredicates { result.removeIf(PathUtils::isIndex); result.add("[]"); } + } + + @Override + public Set getNextKeys(List path) { + Set result = new HashSet<>(); + for (PathPattern pattern : patterns) { + result.addAll(pattern.getNextKeys(path)); + } + coalesceWilds(result); return result; } @@ -139,6 +163,16 @@ public class PathMatcher implements PathPredicates { return result; } + @Override + public Set getPrevKeys(List path) { + Set result = new HashSet<>(); + for (PathPattern pattern : patterns) { + result.addAll(pattern.getPrevKeys(path)); + } + coalesceWilds(result); + return result; + } + @Override public boolean isEmpty() { return patterns.isEmpty(); @@ -152,4 +186,13 @@ public class PathMatcher implements PathPredicates { } return result; } + + @Override + public PathMatcher removeRight(int count) { + PathMatcher result = new PathMatcher(); + for (PathPattern pat : patterns) { + pat.doRemoveRight(count, result); + } + return result; + } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPattern.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPattern.java index 16a22100dd..49c14723bf 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPattern.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPattern.java @@ -48,13 +48,25 @@ public class PathPattern implements PathPredicates { return String.format("", PathUtils.toString(pattern)); } + /** + * Convert this pattern to a string as in {@link PathPredicates#parse(String)}. + * + * @return the string + */ + public String toPatternString() { + return PathUtils.toString(pattern); + } + @Override public boolean equals(Object obj) { if (!(obj instanceof PathPattern)) { return false; } PathPattern that = (PathPattern) obj; - return Objects.equals(this.pattern, that.pattern); + if (!Objects.equals(this.pattern, that.pattern)) { + return false; + } + return true; } @Override @@ -95,6 +107,17 @@ public class PathPattern implements PathPredicates { return true; } + protected boolean matchesBackTo(List path, int length) { + int patternMax = pattern.size() - 1; + int pathMax = path.size() - 1; + for (int i = 0; i < length; i++) { + if (!PathPredicates.keyMatches(pattern.get(patternMax - i), path.get(pathMax - i))) { + return false; + } + } + return true; + } + @Override public boolean matches(List path) { if (path.size() != pattern.size()) { @@ -125,6 +148,17 @@ public class PathPattern implements PathPredicates { return matchesUpTo(path, pattern.size()); } + @Override + public boolean ancestorCouldMatchRight(List path, boolean strict) { + if (path.size() > pattern.size()) { + return false; + } + if (strict && path.size() == pattern.size()) { + return false; + } + return matchesBackTo(path, path.size()); + } + protected static boolean containsWildcards(List pattern) { for (String pat : pattern) { if (isWildcard(pat)) { @@ -142,6 +176,20 @@ public class PathPattern implements PathPredicates { return pattern; } + /** + * Return the pattern as a list of key patterns + * + * @return the list of key patterns + */ + public List asPath() { + return pattern; + } + + /** + * Count the number of wildcard keys in this pattern + * + * @return the count + */ public int countWildcards() { return (int) pattern.stream().filter(k -> isWildcard(k)).count(); } @@ -192,6 +240,17 @@ public class PathPattern implements PathPredicates { return Set.of(); } + @Override + public Set getPrevKeys(List path) { + if (path.size() >= pattern.size()) { + return Set.of(); + } + if (!matchesBackTo(path, path.size())) { + return Set.of(); + } + return Set.of(pattern.get(pattern.size() - 1 - path.size())); + } + @Override public boolean isEmpty() { return false; @@ -254,4 +313,18 @@ public class PathPattern implements PathPredicates { } return result; } + + public void doRemoveRight(int count, PathMatcher result) { + if (count > pattern.size()) { + return; + } + result.addPattern(pattern.subList(0, pattern.size() - count)); + } + + @Override + public PathMatcher removeRight(int count) { + PathMatcher result = new PathMatcher(); + doRemoveRight(count, result); + return result; + } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPredicates.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPredicates.java index 11eb80be93..29f4e63bbf 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPredicates.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPredicates.java @@ -54,10 +54,6 @@ public interface PathPredicates { return new PathPattern(PathUtils.parse(pattern)); } - static PathPredicates all() { - return AllPathsMatcher.INSTANCE; - } - PathPredicates or(PathPredicates that); /** @@ -95,6 +91,18 @@ public interface PathPredicates { */ boolean ancestorMatches(List path, boolean strict); + /** + * Check if the given path could have a matching ancestor, right to left + * + *

+ * This essentially checks if the given path is a viable postfix to the matcher. + * + * @param path the path (postfix) to check + * @param strict true to exclude the case where {@link #matches(List)} would return true + * @return true if an ancestor could match, false otherwise + */ + boolean ancestorCouldMatchRight(List path, boolean strict); + /** * Get the patterns for the next possible key * @@ -130,6 +138,17 @@ public interface PathPredicates { */ Set getNextIndices(List path); + /** + * Get the patterns for the previous possible key (right-to-left matching) + * + *

+ * If an ancestor of the given path cannot match this pattern, the empty set is returned. + * + * @param path the successor path + * @return a set of patterns where indices are enclosed in brackets ({@code []) + */ + Set getPrevKeys(List path); + /** * If this predicate is known to match only one path, i.e., no wildcards, get that path * @@ -144,6 +163,14 @@ public interface PathPredicates { */ PathPattern getSingletonPattern(); + /** + * Remove count elements from the right + * + * @param count the number of elements to remove + * @return the resulting predicates + */ + PathPredicates removeRight(int count); + default NavigableMap, ?> getCachedValues(TargetObject seed) { return getCachedValues(List.of(), seed); } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/PathPredicatesTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/PathPredicatesTest.java new file mode 100644 index 0000000000..808dc2c21a --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/PathPredicatesTest.java @@ -0,0 +1,41 @@ +/* ### + * 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.dbg.util; + +import static org.junit.Assert.assertEquals; + +import java.util.Set; + +import org.junit.Test; + +public class PathPredicatesTest { + @Test + public void testGetPrevKeys() { + PathPredicates pred = PathPredicates.parse("Processes[0].Threads[].Stack"); + + assertEquals(Set.of("Stack"), pred.getPrevKeys(PathUtils.parse(""))); + assertEquals(Set.of("[]"), pred.getPrevKeys(PathUtils.parse("Stack"))); + assertEquals(Set.of("Threads"), pred.getPrevKeys(PathUtils.parse("[].Stack"))); + assertEquals(Set.of("[0]"), pred.getPrevKeys(PathUtils.parse("Threads[].Stack"))); + assertEquals(Set.of("Processes"), pred.getPrevKeys(PathUtils.parse("[0].Threads[].Stack"))); + assertEquals(Set.of(), pred.getPrevKeys(PathUtils.parse("Processes[0].Threads[].Stack"))); + + assertEquals(Set.of(), + pred.getPrevKeys(PathUtils.parse("Foo.Processes[0].Threads[].Stack"))); + assertEquals(Set.of(), pred.getPrevKeys(PathUtils.parse("Foo"))); + assertEquals(Set.of(), pred.getPrevKeys(PathUtils.parse("[]"))); + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceObjectBreakpointLocation.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceObjectBreakpointLocation.java index c384b7da21..fbfde599a5 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceObjectBreakpointLocation.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceObjectBreakpointLocation.java @@ -277,7 +277,7 @@ public class DBTraceObjectBreakpointLocation } PathMatcher procMatcher = schema.searchFor(TargetProcess.class, false); - return object.getAncestors(getLifespan(), procMatcher) + return object.getAncestorsRoot(getLifespan(), procMatcher) .flatMap(proc -> proc.getSource(object) .querySuccessorsInterface(getLifespan(), TraceObjectThread.class)) diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObject.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObject.java index 4c88fc207d..19debaeaca 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObject.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObject.java @@ -203,6 +203,7 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { @Override public RangeSet getLife() { + // TODO: This should really be cached try (LockHold hold = manager.trace.lockRead()) { RangeSet result = TreeRangeSet.create(); // NOTE: connected ranges should already be coalesced @@ -220,19 +221,20 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { return manager.doGetObject(path.parent()); } - protected void doInsert(Range lifespan, ConflictResolution resolution) { + protected DBTraceObjectValPath doInsert(Range lifespan, ConflictResolution resolution) { if (path.isRoot()) { - return; + return DBTraceObjectValPath.of(); } DBTraceObject parent = doCreateCanonicalParentObject(); - parent.setValue(lifespan, path.key(), this, resolution); - parent.doInsert(lifespan, resolution); + InternalTraceObjectValue value = parent.setValue(lifespan, path.key(), this, resolution); + DBTraceObjectValPath path = parent.doInsert(lifespan, resolution); + return path.append(value); } @Override - public void insert(Range lifespan, ConflictResolution resolution) { + public DBTraceObjectValPath insert(Range lifespan, ConflictResolution resolution) { try (LockHold hold = manager.trace.lockWrite()) { - doInsert(lifespan, resolution); + return doInsert(lifespan, resolution); } } @@ -253,6 +255,9 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { } protected void doRemoveTree(Range span) { + for (DBTraceObjectValue parent : getParents()) { + parent.doTruncateOrDeleteAndEmitLifeChange(span); + } for (InternalTraceObjectValue value : getValues()) { value.doTruncateOrDeleteAndEmitLifeChange(span); if (value.isCanonical()) { @@ -264,7 +269,6 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { @Override public void removeTree(Range span) { try (LockHold hold = manager.trace.lockWrite()) { - getCanonicalParents(span).forEach(v -> v.doTruncateOrDeleteAndEmitLifeChange(span)); doRemoveTree(span); } } @@ -327,10 +331,14 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { return ifCls.cast(ifaces.get(ifCls)); } + protected Collection doGetParents() { + return manager.valuesByChild.get(this); + } + @Override public Collection getParents() { try (LockHold hold = manager.trace.lockRead()) { - return manager.valuesByChild.get(this); + return doGetParents(); } } @@ -626,23 +634,35 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { } @Override - public Stream getAncestors( + public Stream getAncestors(Range span, + PathPredicates relativePredicates) { + try (LockHold hold = manager.trace.lockRead()) { + Stream ancestors = + doStreamVisitor(span, new InternalAncestorsRelativeVisitor(relativePredicates)); + if (relativePredicates.matches(List.of())) { + return Stream.concat(Stream.of(DBTraceObjectValPath.of()), ancestors); + } + return ancestors; + } + } + + @Override + public Stream getAncestorsRoot( Range span, PathPredicates rootPredicates) { try (LockHold hold = manager.trace.lockRead()) { - return doStreamVisitor(span, new InternalAncestorsVisitor(rootPredicates)); + return doStreamVisitor(span, new InternalAncestorsRootVisitor(rootPredicates)); } } @Override public Stream getSuccessors( Range span, PathPredicates relativePredicates) { - DBTraceObjectValPath empty = DBTraceObjectValPath.of(); try (LockHold hold = manager.trace.lockRead()) { Stream succcessors = - doStreamVisitor(span, new InternalSuccessorsVisitor(relativePredicates)); + doStreamVisitor(span, new InternalSuccessorsRelativeVisitor(relativePredicates)); if (relativePredicates.matches(List.of())) { // Pre-cat the empty path (not the empty stream) - return Stream.concat(Stream.of(empty), succcessors); + return Stream.concat(Stream.of(DBTraceObjectValPath.of()), succcessors); } return succcessors; } @@ -794,7 +814,7 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { Class targetIf) { // This is a sort of meet-in-the-middle. The type search must originate from the root PathMatcher matcher = getManager().getRootSchema().searchFor(targetIf, false); - return getAncestors(span, matcher); + return getAncestorsRoot(span, matcher); } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectAddressRangeValue.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectAddressRangeValue.java index 0cc2825900..c912eb6c48 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectAddressRangeValue.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectAddressRangeValue.java @@ -23,6 +23,7 @@ import ghidra.trace.database.map.DBTraceAddressSnapRangePropertyMapTree; import ghidra.trace.database.map.DBTraceAddressSnapRangePropertyMapTree.AbstractDBTraceAddressSnapRangePropertyMapData; import ghidra.trace.database.target.DBTraceObjectValue.DBTraceObjectDBFieldCodec; import ghidra.trace.model.Trace; +import ghidra.trace.model.target.TraceObjectKeyPath; import ghidra.trace.model.target.TraceObjectValue; import ghidra.trace.util.TraceAddressSpace; import ghidra.util.LockHold; @@ -66,6 +67,12 @@ public class DBTraceObjectAddressRangeValue this.manager = manager; } + @Override + public String toString() { + return getClass().getSimpleName() + ": parent=" + parent + ", key=" + entryKey + + ", lifespan=" + getLifespan() + ", value=" + getValue(); + } + @Override protected void setRecordValue(DBTraceObjectAddressRangeValue value) { // Nothing to do. I am the value @@ -121,11 +128,23 @@ public class DBTraceObjectAddressRangeValue throw new ClassCastException(); } + @Override + public boolean isObject() { + return false; + } + @Override public DBTraceObject getChildOrNull() { return null; } + @Override + public TraceObjectKeyPath getCanonicalPath() { + try (LockHold hold = manager.trace.lockRead()) { + return parent.getCanonicalPath().extend(entryKey); + } + } + @Override public boolean isCanonical() { return false; diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectManager.java index 75eaa04d19..f19271e259 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectManager.java @@ -368,7 +368,7 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager if (rootVal == null) { return Stream.of(); } - return rootVal.doStreamVisitor(span, new InternalSuccessorsVisitor(predicates)); + return rootVal.doStreamVisitor(span, new InternalSuccessorsRelativeVisitor(predicates)); } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValue.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValue.java index b18677e30d..af0984c8b8 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValue.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValue.java @@ -31,6 +31,7 @@ import ghidra.trace.database.DBTraceUtils; import ghidra.trace.database.target.InternalTreeTraversal.Visitor; import ghidra.trace.model.Trace; import ghidra.trace.model.target.TraceObject; +import ghidra.trace.model.target.TraceObjectKeyPath; import ghidra.util.LockHold; import ghidra.util.database.*; import ghidra.util.database.DBCachedObjectStoreFactory.AbstractDBFieldCodec; @@ -275,6 +276,11 @@ public class DBTraceObjectValue extends DBAnnotatedObject implements InternalTra return (DBTraceObject) getValue(); } + @Override + public boolean isObject() { + return child != null; + } + @Override public DBTraceObject getChildOrNull() { return child; @@ -320,6 +326,10 @@ public class DBTraceObjectValue extends DBAnnotatedObject implements InternalTra return InternalTreeTraversal.INSTANCE.walkValue(visitor, this, span, null); } + protected TraceObjectKeyPath doGetCanonicalPath() { + return triple.parent.getCanonicalPath().extend(triple.key); + } + protected boolean doIsCanonical() { if (child == null) { return false; @@ -327,7 +337,14 @@ public class DBTraceObjectValue extends DBAnnotatedObject implements InternalTra if (triple.parent == null) { return true; } - return triple.parent.getCanonicalPath().extend(triple.key).equals(child.getCanonicalPath()); + return doGetCanonicalPath().equals(child.getCanonicalPath()); + } + + @Override + public TraceObjectKeyPath getCanonicalPath() { + try (LockHold hold = LockHold.lock(manager.lock.readLock())) { + return doGetCanonicalPath(); + } } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalAncestorsRelativeVisitor.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalAncestorsRelativeVisitor.java new file mode 100644 index 0000000000..e1d0322537 --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalAncestorsRelativeVisitor.java @@ -0,0 +1,66 @@ +/* ### + * 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.trace.database.target; + +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import com.google.common.collect.Range; + +import ghidra.dbg.util.PathPredicates; +import ghidra.trace.database.target.InternalTreeTraversal.SpanIntersectingVisitor; +import ghidra.trace.database.target.InternalTreeTraversal.VisitResult; + +public class InternalAncestorsRelativeVisitor implements SpanIntersectingVisitor { + + protected final PathPredicates predicates; + + public InternalAncestorsRelativeVisitor(PathPredicates predicates) { + this.predicates = predicates; + } + + @Override + public DBTraceObjectValPath composePath(DBTraceObjectValPath pre, + InternalTraceObjectValue value) { + return pre == null ? DBTraceObjectValPath.of() : pre.prepend(value); + } + + @Override + public VisitResult visitValue(InternalTraceObjectValue value, DBTraceObjectValPath path) { + List keyList = path.getKeyList(); + return VisitResult.result(predicates.matches(keyList), + predicates.ancestorCouldMatchRight(keyList, true) && value.getChildOrNull() != null); + } + + @Override + public DBTraceObject continueObject(InternalTraceObjectValue value) { + return value.getParent(); + } + + @Override + public Stream continueValues(DBTraceObject object, + Range span, DBTraceObjectValPath pre) { + Set prevKeys = predicates.getPrevKeys(pre.getKeyList()); + if (prevKeys.isEmpty()) { + return Stream.empty(); + } + + return object.doGetParents() + .stream() + .filter(v -> PathPredicates.anyMatches(prevKeys, v.getEntryKey())); + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalAncestorsVisitor.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalAncestorsRootVisitor.java similarity index 93% rename from Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalAncestorsVisitor.java rename to Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalAncestorsRootVisitor.java index 8a0f087175..92ecbaaeb2 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalAncestorsVisitor.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalAncestorsRootVisitor.java @@ -23,11 +23,11 @@ import ghidra.dbg.util.PathPredicates; import ghidra.trace.database.target.InternalTreeTraversal.SpanIntersectingVisitor; import ghidra.trace.database.target.InternalTreeTraversal.VisitResult; -public class InternalAncestorsVisitor implements SpanIntersectingVisitor { +public class InternalAncestorsRootVisitor implements SpanIntersectingVisitor { protected final PathPredicates predicates; - public InternalAncestorsVisitor(PathPredicates predicates) { + public InternalAncestorsRootVisitor(PathPredicates predicates) { this.predicates = predicates; } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalSuccessorsVisitor.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalSuccessorsRelativeVisitor.java similarity index 94% rename from Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalSuccessorsVisitor.java rename to Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalSuccessorsRelativeVisitor.java index e8ab73d9c0..ec13c70842 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalSuccessorsVisitor.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalSuccessorsRelativeVisitor.java @@ -26,11 +26,11 @@ import ghidra.trace.database.DBTraceUtils; import ghidra.trace.database.target.InternalTreeTraversal.SpanIntersectingVisitor; import ghidra.trace.database.target.InternalTreeTraversal.VisitResult; -public class InternalSuccessorsVisitor implements SpanIntersectingVisitor { +public class InternalSuccessorsRelativeVisitor implements SpanIntersectingVisitor { protected final PathPredicates predicates; - public InternalSuccessorsVisitor(PathPredicates predicates) { + public InternalSuccessorsRelativeVisitor(PathPredicates predicates) { this.predicates = predicates; } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/TraceDomainObjectListener.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/TraceDomainObjectListener.java index c79a4fea13..4c8554cdd7 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/TraceDomainObjectListener.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/TraceDomainObjectListener.java @@ -125,6 +125,14 @@ public class TraceDomainObjectListener implements DomainObjectListener { typedMap.put(type, handler); } + /** + * Listen for the given event, taking the affected object, the old value, and the new value + * + * @param the type of the affected object + * @param the type of the values + * @param type the event type + * @param handler the handler + */ protected void listenFor(TraceChangeType type, AffectedAndValuesOnlyHandler handler) { typedMap.put(type, handler); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObject.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObject.java index 2fa7f799b8..d737e8ea4e 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObject.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObject.java @@ -21,6 +21,7 @@ import java.util.stream.Stream; import com.google.common.collect.Range; import com.google.common.collect.RangeSet; +import ghidra.dbg.target.TargetMethod; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.util.PathPredicates; @@ -38,6 +39,8 @@ import ghidra.trace.model.TraceUniqueObject; * In many cases, such interfaces are just wrappers. */ public interface TraceObject extends TraceUniqueObject { + String EXTRA_INTERFACES_ATTRIBUTE_NAME = "_extra_ifs"; + /** * Get the trace containing this object * @@ -79,8 +82,9 @@ public interface TraceObject extends TraceUniqueObject { * * @param the minimum lifespan of edges from the root to this object * @param resolution the rule for handling duplicate keys when setting values. + * @return the value path from root to the newly inserted object */ - void insert(Range lifespan, ConflictResolution resolution); + TraceObjectValPath insert(Range lifespan, ConflictResolution resolution); /** * Remove this object from its canonical path for the given lifespan @@ -97,9 +101,9 @@ public interface TraceObject extends TraceUniqueObject { * Remove this object and its successors from their canonical paths for the given span * *

- * Truncate the lifespans of this object's canonical parent value and all canonical values - * succeeding this object. If a truncated value's lifespan is contained in the given span, the - * value will be deleted. + * Truncate the lifespans of this object's parent values and all canonical values succeeding + * this object. If a truncated value's lifespan is contained in the given span, the value will + * be deleted. * * @param span the span during which this object and its canonical successors should be removed */ @@ -282,9 +286,20 @@ public interface TraceObject extends TraceUniqueObject { * @param rootPredicates the predicates for matching path keys, relative to the root * @return the stream of matching paths to values */ - Stream getAncestors(Range span, + Stream getAncestorsRoot(Range span, PathPredicates rootPredicates); + /** + * Stream all ancestor values of this object matching the given predicates, intersecting the + * given span + * + * @param span a span which values along the path must intersect + * @param relativePredicates the predicates for matching path keys, relative to this object + * @return the stream of matching paths to values + */ + Stream getAncestors(Range span, + PathPredicates relativePredicates); + /** * Stream all successor values of this object matching the given predicates, intersecting the * given span @@ -466,4 +481,30 @@ public interface TraceObject extends TraceUniqueObject { */ @Override boolean isDeleted(); + + /** + * Check if the child represents a method at the given snap + * + * @param snap the snap + * @return true if a method + */ + default boolean isMethod(long snap) { + if (getTargetSchema().getInterfaces().contains(TargetMethod.class)) { + return true; + } + TraceObjectValue extras = getAttribute(snap, TraceObject.EXTRA_INTERFACES_ATTRIBUTE_NAME); + if (extras == null) { + return false; + } + Object val = extras.getValue(); + if (!(val instanceof String)) { + return false; + } + String valStr = (String) val; + // Not ideal, but it's not a substring of any other schema interface.... + if (valStr.contains("Method")) { + return true; + } + return false; + } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObjectKeyPath.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObjectKeyPath.java index 3220afec6b..0c9165c8f4 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObjectKeyPath.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObjectKeyPath.java @@ -225,9 +225,26 @@ public final class TraceObjectKeyPath implements Comparable if (!predicates.ancestorMatches(keyList, false)) { return Stream.of(); } + Stream ancestry = + isRoot() ? Stream.of() : parent().streamMatchingAncestry(predicates); if (predicates.matches(keyList)) { - return Stream.concat(Stream.of(this), parent().streamMatchingAncestry(predicates)); + return Stream.concat(Stream.of(this), ancestry); } - return parent().streamMatchingAncestry(predicates); + return ancestry; + } + + /** + * Check if this path is an ancestor of the given path + * + *

+ * Equivalently, check if the given path is a successor of this path. A path is considered an + * ancestor of itself. To check for a strict ancestor, use + * {@code this.isAncestor(that) && !this.equals(that)}. + * + * @param that the supposed successor to this path + * @return true if the given path is in fact a successor + */ + public boolean isAncestor(TraceObjectKeyPath that) { + return PathUtils.isAncestor(keyList, that.keyList); } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObjectValue.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObjectValue.java index c6f8b460f7..f76c0955ce 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObjectValue.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObjectValue.java @@ -17,6 +17,7 @@ package ghidra.trace.model.target; import com.google.common.collect.Range; +import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.trace.model.Trace; import ghidra.trace.model.target.TraceObject.ConflictResolution; @@ -43,6 +44,17 @@ public interface TraceObjectValue { */ String getEntryKey(); + /** + * Get the "canonical path" of this value + * + *

+ * This is the parent's canonical path extended by this value's entry key. Note, in the case + * this value has a child object, this is not necessarily its canonical path. + * + * @return + */ + TraceObjectKeyPath getCanonicalPath(); + /** * Get the value * @@ -58,6 +70,13 @@ public interface TraceObjectValue { */ TraceObject getChild(); + /** + * Check if the value is an object (i.e., {@link TraceObject}) + * + * @return true if an object, false otherwise + */ + boolean isObject(); + /** * Check if this value represents its child's canonical location * @@ -69,6 +88,15 @@ public interface TraceObjectValue { */ boolean isCanonical(); + /** + * Get the (target) schema for the value + * + * @return the schema + */ + default TargetObjectSchema getTargetSchema() { + return getParent().getTargetSchema().getChildSchema(getEntryKey()); + } + /** * Set the lifespan of this entry, truncating duplicates * @@ -157,4 +185,17 @@ public interface TraceObjectValue { * if a second is created. */ TraceObjectValue truncateOrDelete(Range span); + + /** + * Check if the schema designates this value as hidden + * + * @return true if hidden + */ + default boolean isHidden() { + TraceObject parent = getParent(); + if (parent == null) { + return false; + } + return parent.getTargetSchema().isHidden(getEntryKey()); + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeCursorTableHeaderRenderer.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeCursorTableHeaderRenderer.java index ff7b84722a..946f923eba 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeCursorTableHeaderRenderer.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeCursorTableHeaderRenderer.java @@ -26,21 +26,24 @@ import javax.swing.table.*; import com.google.common.collect.Range; public class RangeCursorTableHeaderRenderer> - extends GTableHeaderRenderer { + extends GTableHeaderRenderer implements RangedRenderer { protected final static int ARROW_SIZE = 10; protected final static Polygon ARROW = new Polygon( new int[] { 0, -ARROW_SIZE, -ARROW_SIZE }, new int[] { 0, ARROW_SIZE, -ARROW_SIZE }, 3); - protected Range fullRange = Range.closed(0d, 1d); + protected Range fullRangeDouble = Range.closed(0d, 1d); protected double span = 1; + protected Range fullRange; + protected N pos; protected double doublePos; + @Override public void setFullRange(Range fullRange) { - this.fullRange = RangeTableCellRenderer.validateViewRange(fullRange); - this.span = this.fullRange.upperEndpoint() - this.fullRange.lowerEndpoint(); + this.fullRangeDouble = RangedRenderer.validateViewRange(fullRange); + this.span = this.fullRangeDouble.upperEndpoint() - this.fullRangeDouble.lowerEndpoint(); } public void setCursorPosition(N pos) { @@ -51,6 +54,7 @@ public class RangeCursorTableHeaderRenderer> @Override protected void paintChildren(Graphics g) { super.paintChildren(g); + // The cursor should occlude the children paintCursor(g); } @@ -58,7 +62,7 @@ public class RangeCursorTableHeaderRenderer> Graphics2D g = (Graphics2D) parentG.create(); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - double x = (doublePos - fullRange.lowerEndpoint()) / span * getWidth(); + double x = (doublePos - fullRangeDouble.lowerEndpoint()) / span * getWidth(); g.translate(x, getHeight()); g.rotate(Math.PI / 2); g.setColor(getForeground()); @@ -117,7 +121,8 @@ public class RangeCursorTableHeaderRenderer> } TableColumn col = colModel.getColumn(viewColIdx); - double pos = span * (e.getX() - colX) / col.getWidth() + fullRange.lowerEndpoint(); + double pos = + span * (e.getX() - colX) / col.getWidth() + fullRangeDouble.lowerEndpoint(); listener.accept(pos); } }; @@ -128,4 +133,19 @@ public class RangeCursorTableHeaderRenderer> public N getCursorPosition() { return pos; } + + @Override + public Range getFullRange() { + return fullRange; + } + + @Override + public Range getFullRangeDouble() { + return fullRangeDouble; + } + + @Override + public double getSpan() { + return span; + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeSetTableCellRenderer.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeSetTableCellRenderer.java new file mode 100644 index 0000000000..d54afcbea7 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeSetTableCellRenderer.java @@ -0,0 +1,84 @@ +/* ### + * 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 docking.widgets.table; + +import java.awt.Component; +import java.awt.Graphics; + +import com.google.common.collect.Range; +import com.google.common.collect.RangeSet; + +import ghidra.docking.settings.Settings; +import ghidra.util.table.column.AbstractGColumnRenderer; + +public class RangeSetTableCellRenderer> + extends AbstractGColumnRenderer> implements RangedRenderer { + protected Range fullRangeDouble = Range.closed(0d, 1d); + protected double span = 1; + + protected Range fullRange; + protected RangeSet dataRangeSet; + + @Override + public void setFullRange(Range fullRange) { + this.fullRange = fullRange; + this.fullRangeDouble = RangedRenderer.validateViewRange(fullRange); + this.span = this.fullRangeDouble.upperEndpoint() - this.fullRangeDouble.lowerEndpoint(); + } + + @Override + public String getFilterString(RangeSet t, Settings settings) { + return ""; + } + + @Override + @SuppressWarnings("unchecked") + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + this.dataRangeSet = (RangeSet) data.getValue(); + super.getTableCellRendererComponent(data); + setText(""); + return this; + } + + @Override + protected void paintComponent(Graphics parentG) { + super.paintComponent(parentG); + if (dataRangeSet == null || dataRangeSet.isEmpty()) { + return; + } + + Graphics g = parentG.create(); + g.setColor(getForeground()); + for (Range range : dataRangeSet.asRanges()) { + paintRange(g, range); + } + } + + @Override + public Range getFullRange() { + return fullRange; + } + + @Override + public Range getFullRangeDouble() { + return fullRangeDouble; + } + + @Override + public double getSpan() { + return span; + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeTableCellRenderer.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeTableCellRenderer.java index 93a3ca207d..08c96b97dd 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeTableCellRenderer.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeTableCellRenderer.java @@ -24,27 +24,19 @@ import ghidra.docking.settings.Settings; import ghidra.util.table.column.AbstractGColumnRenderer; public class RangeTableCellRenderer> - extends AbstractGColumnRenderer> { + extends AbstractGColumnRenderer> implements RangedRenderer { - protected Range doubleFullRange = Range.closed(0d, 1d); + protected Range fullRangeDouble = Range.closed(0d, 1d); protected double span = 1; protected Range fullRange; protected Range dataRange; - public static Range validateViewRange(Range fullRange) { - if (!fullRange.hasLowerBound() || !fullRange.hasUpperBound()) { - throw new IllegalArgumentException("Cannot have unbounded full range"); - } - // I don't care to preserve open/closed, since it just specifies the view bounds - return Range.closed(fullRange.lowerEndpoint().doubleValue(), - fullRange.upperEndpoint().doubleValue()); - } - + @Override public void setFullRange(Range fullRange) { this.fullRange = fullRange; - this.doubleFullRange = validateViewRange(fullRange); - this.span = this.doubleFullRange.upperEndpoint() - this.doubleFullRange.lowerEndpoint(); + this.fullRangeDouble = RangedRenderer.validateViewRange(fullRange); + this.span = this.fullRangeDouble.upperEndpoint() - this.fullRangeDouble.lowerEndpoint(); } @Override @@ -67,38 +59,24 @@ public class RangeTableCellRenderer> if (dataRange == null) { return; } - int width = getWidth(); - int height = getHeight(); - - int x1 = dataRange.hasLowerBound() - ? interpolate(width, dataRange.lowerEndpoint().doubleValue()) - : 0; - int x2 = dataRange.hasUpperBound() - ? interpolate(width, dataRange.upperEndpoint().doubleValue()) - : width; - - int y1 = height > 2 ? 1 : 0; - int y2 = height > 2 ? height - 1 : height; Graphics g = parentG.create(); g.setColor(getForeground()); - - g.fillRect(x1, y1, x2 - x1, y2 - y1); - } - - protected int interpolate(int w, double val) { - double lower = doubleFullRange.lowerEndpoint(); - if (val <= lower) { - return 0; - } - if (val >= doubleFullRange.upperEndpoint()) { - return w; - } - double dif = val - lower; - return (int) (dif / span * w); + paintRange(g, dataRange); } + @Override public Range getFullRange() { return fullRange; } + + @Override + public Range getFullRangeDouble() { + return fullRangeDouble; + } + + @Override + public double getSpan() { + return span; + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangedRenderer.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangedRenderer.java new file mode 100644 index 0000000000..a73baeb1fa --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangedRenderer.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 docking.widgets.table; + +import java.awt.Graphics; + +import com.google.common.collect.Range; + +public interface RangedRenderer> { + + public static Range validateViewRange(Range fullRange) { + if (!fullRange.hasLowerBound() || !fullRange.hasUpperBound()) { + throw new IllegalArgumentException("Cannot have unbounded full range"); + } + // I don't care to preserve open/closed, since it just specifies the view bounds + return Range.closed(fullRange.lowerEndpoint().doubleValue(), + fullRange.upperEndpoint().doubleValue()); + } + + void setFullRange(Range fullRange); + + Range getFullRange(); + + Range getFullRangeDouble(); + + double getSpan(); + + default int interpolate(int w, double val) { + Range fullRangeDouble = getFullRangeDouble(); + double span = getSpan(); + double lower = fullRangeDouble.lowerEndpoint(); + if (val <= lower) { + return 0; + } + if (val >= fullRangeDouble.upperEndpoint()) { + return w; + } + double dif = val - lower; + return (int) (dif / span * w); + } + + int getWidth(); + + int getHeight(); + + default void paintRange(Graphics g, Range range) { + int width = getWidth(); + int height = getHeight(); + + int x1 = range.hasLowerBound() + ? interpolate(width, range.lowerEndpoint().doubleValue()) + : 0; + int x2 = range.hasUpperBound() + ? interpolate(width, range.upperEndpoint().doubleValue()) + : width; + + int y1 = height > 2 ? 1 : 0; + int y2 = height > 2 ? height - 1 : height; + + g.fillRect(x1, y1, x2 - x1, y2 - y1); + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/AbstractDynamicTableColumn.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/AbstractDynamicTableColumn.java index 6e74b0ca72..47a4f3c9ac 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/AbstractDynamicTableColumn.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/AbstractDynamicTableColumn.java @@ -24,18 +24,17 @@ import ghidra.util.table.column.GColumnRenderer; import utilities.util.reflection.ReflectionUtilities; /** - * An Table Column is an interface that should be implemented by each class that provides - * a field (column) of an object based table (each row relates to a particular type of object). - * It determines the appropriate cell object for use by the table column this field represents. - * It can then return the appropriate object to display in the table cell for the indicated - * row object. + * An Table Column is an interface that should be implemented by each class that provides a field + * (column) of an object based table (each row relates to a particular type of object). It + * determines the appropriate cell object for use by the table column this field represents. It can + * then return the appropriate object to display in the table cell for the indicated row object. * * Implementations of this interface must provide a public default constructor. * * @param The row object class supported by this column * @param The column object class supported by this column - * @param The object class type that will be passed to - * see getValue(ROW_TYPE, Settings, DATA_SOURCE, ServiceProvider) + * @param The object class type that will be passed to see + * getValue(ROW_TYPE, Settings, DATA_SOURCE, ServiceProvider) */ public abstract class AbstractDynamicTableColumn implements DynamicTableColumn { @@ -80,11 +79,16 @@ public abstract class AbstractDynamicTableColumn getComparator() { return null; } + @Override + public Comparator getComparator(DynamicColumnTableModel model, + int columnIndex) { + return getComparator(); + } + @Override @SuppressWarnings("unchecked") // enforced by the compiler diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/DynamicTableColumn.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/DynamicTableColumn.java index 9b5f26c2e3..512f2945cf 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/DynamicTableColumn.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/DynamicTableColumn.java @@ -24,48 +24,52 @@ import ghidra.framework.plugintool.ServiceProvider; import ghidra.util.table.column.GColumnRenderer; /** - * The root interface for defining columns for {@link GDynamicColumnTableModel}s. The - * class allows you to create objects for tables that know how to give a column value for a - * given row. + * The root interface for defining columns for {@link DynamicColumnTableModel}s. The class allows + * you to create objects for tables that know how to give a column value for a given row. * * @param The row object class supported by this column * @param The column object class supported by this column - * @param The object class type that will be passed to - * see getValue(ROW_TYPE, Settings, DATA_SOURCE, ServiceProvider) + * @param The object class type that will be passed to see + * getValue(ROW_TYPE, Settings, DATA_SOURCE, ServiceProvider) */ public interface DynamicTableColumn { /** - * Determines the unique column heading that may be used to identify a column instance. - * This name must be non-changing and is used to save/restore state information. + * Determines the unique column heading that may be used to identify a column instance. This + * name must be non-changing and is used to save/restore state information. + * * @return the field instance name. */ public String getColumnName(); /** * Determines the class of object that is associated with this field (column). + * * @return the column class */ public Class getColumnClass(); /** - * Returns the preferred width for this column. Column should either return a valid positive + * Returns the preferred width for this column. Column should either return a valid positive * preferred size or -1. + * * @return the preferred width for this column. */ public int getColumnPreferredWidth(); /** - * Returns the single class type of the data that this table field can use to - * generate columnar data. - * @return the single class type of the data that this table field can use to - * generate columnar data. + * Returns the single class type of the data that this table field can use to generate columnar + * data. + * + * @return the single class type of the data that this table field can use to generate columnar + * data. */ public Class getSupportedRowType(); /** - * Creates an object that is appropriate for this field (table column) and for the - * object that is associated with this row of the table. + * Creates an object that is appropriate for this field (table column) and for the object that + * is associated with this row of the table. + * * @param rowObject the object associated with the row in the table. * @param settings field settings * @param data the expected data object, as defined by the DATA_SOURCE type @@ -79,13 +83,15 @@ public interface DynamicTableColumn { /** * Returns the optional cell renderer for this column; null if no renderer is used. * - *

This method allows columns to define custom rendering. The interface returned here - * ensures that the text used for filtering matches what the users sees (via the + *

+ * This method allows columns to define custom rendering. The interface returned here ensures + * that the text used for filtering matches what the users sees (via the * {@link GColumnRenderer#getFilterString(Object, Settings)} method). * - *

Note: some types should not make use of the aforementioned filter string. These types - * include the {@link Number} wrapper types, {@link Date} and {@link Enum}s. (This is - * because the filtering system works naturally with these types.) See {@link GColumnRenderer}. + *

+ * Note: some types should not make use of the aforementioned filter string. These types include + * the {@link Number} wrapper types, {@link Date} and {@link Enum}s. (This is because the + * filtering system works naturally with these types.) See {@link GColumnRenderer}. * * @return the renderer */ @@ -93,13 +99,15 @@ public interface DynamicTableColumn { /** * Returns a list of settings definitions for this field. + * * @return list of settings definitions for this field. */ public SettingsDefinition[] getSettingsDefinitions(); /** - * Gets the maximum number of text display lines needed for any given cell with the - * specified settings. + * Gets the maximum number of text display lines needed for any given cell with the specified + * settings. + * * @param settings field settings * @return maximum number of lines needed */ @@ -107,28 +115,33 @@ public interface DynamicTableColumn { /** * Determines the column heading that will be displayed. + * * @param settings the settings * @return the field name to display as the column heading. */ public String getColumnDisplayName(Settings settings); /** - * Returns a description of this column. This may be used as a tooltip for the column header - * @return a description of this column. This may be used as a tooltip for the column header. + * Returns a description of this column. This may be used as a tooltip for the column header + * + * @return a description of this column. This may be used as a tooltip for the column header. */ public String getColumnDescription(); /** - * Returns a value that is unique for this table column. This is different than getting - * the display name, which may be shared by different columns. + * Returns a value that is unique for this table column. This is different than getting the + * display name, which may be shared by different columns. + * * @return the identifier */ public String getUniqueIdentifier(); /** - * If implemented, will return a comparator that knows how to sort values for this column. + * If implemented, will return a comparator that knows how to sort values for this column. * Implementors should return null if they do not wish to provider a comparator + * * @return the comparator */ - public Comparator getComparator(); + public Comparator getComparator(DynamicColumnTableModel model, + int columnIndex); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GDynamicColumnTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GDynamicColumnTableModel.java index adf84dcde6..c9dadd14d1 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GDynamicColumnTableModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GDynamicColumnTableModel.java @@ -31,31 +31,29 @@ import util.CollectionUtils; import utilities.util.reflection.ReflectionUtilities; /** - * An abstract table model for showing DynamicTableColumns where each row is based on an - * object of type ROW_TYPE. The client is responsible for implementing - * {@link #createTableColumnDescriptor()}. This method specifies which default columns the - * table should have and whether they should be visible or hidden. Hidden columns can be - * made visible through the UI. + * An abstract table model for showing DynamicTableColumns where each row is based on an object of + * type ROW_TYPE. The client is responsible for implementing {@link #createTableColumnDescriptor()}. + * This method specifies which default columns the table should have and whether they should be + * visible or hidden. Hidden columns can be made visible through the UI. *

* This model will also discover other system columns that understand how to render - * ROW_TYPE data directly. Also, if you create a {@link TableRowMapper mapper}(s) for + * ROW_TYPE data directly. Also, if you create a {@link TableRowMapper mapper}(s) for * your row type, then this model will load columns for each type for which a mapper was created, * all as optional, hidden columns. *

- * The various attributes of the columns of this model (visibility, position, size, etc) are - * saved to disk as tool preferences when the user exits the tool. + * The various attributes of the columns of this model (visibility, position, size, etc) are saved + * to disk as tool preferences when the user exits the tool. *

* Implementation Note: this model loads all columns, specific and discovered, as being visible. - * Then, during initialization, the {@link TableColumnModelState} class will - * either hide all non-default columns, or reload the column state if any - * previous saved state is found. + * Then, during initialization, the {@link TableColumnModelState} class will either hide all + * non-default columns, or reload the column state if any previous saved state is found. * * @param the row object class for this table model. - * @param the type of data that will be returned from {@link #getDataSource()}. This - * object will be given to the {@link DynamicTableColumn} objects used by this - * table model when - * {@link DynamicTableColumn#getValue(Object, ghidra.docking.settings.Settings, Object, ServiceProvider)} - * is called. + * @param the type of data that will be returned from {@link #getDataSource()}. This + * object will be given to the {@link DynamicTableColumn} objects used by this table + * model when + * {@link DynamicTableColumn#getValue(Object, ghidra.docking.settings.Settings, Object, ServiceProvider)} + * is called. */ public abstract class GDynamicColumnTableModel extends AbstractSortedTableModel @@ -122,10 +120,10 @@ public abstract class GDynamicColumnTableModel } /** - * Allows clients to defer column creation until after this parent class's constructor has - * been called. This method will not restore any column settings that have been changed - * after construction. Thus, this method is intended only to be called during the - * construction process. + * Allows clients to defer column creation until after this parent class's constructor has been + * called. This method will not restore any column settings that have been changed after + * construction. Thus, this method is intended only to be called during the construction + * process. */ protected void reloadColumns() { @@ -201,9 +199,8 @@ public abstract class GDynamicColumnTableModel } /** - * This differs from {@link #createSortComparator(int)} in that the other method - * creates a comparator that operates on a full row value, whereas this method operates on - * column values. + * This differs from {@link #createSortComparator(int)} in that the other method creates a + * comparator that operates on a full row value, whereas this method operates on column values. * * @param columnIndex the column index * @return a comparator for the specific column values @@ -211,7 +208,8 @@ public abstract class GDynamicColumnTableModel @SuppressWarnings("unchecked") // the column provides the values itself; safe cast protected Comparator createSortComparatorForColumn(int columnIndex) { DynamicTableColumn column = getColumn(columnIndex); - Comparator comparator = (Comparator) column.getComparator(); + Comparator comparator = + (Comparator) column.getComparator(this, columnIndex); return comparator; } @@ -252,11 +250,13 @@ public abstract class GDynamicColumnTableModel } /** - * Adds the given column at the end of the list of columns. This method is intended for + * Adds the given column at the end of the list of columns. This method is intended for * implementations to add custom column objects, rather than relying on generic, discovered * DynamicTableColumn implementations. * - *

Note: this method assumes that the columns have already been sorted + *

+ * Note: this method assumes that the columns have already been sorted + * * @param column The field to add */ protected void addTableColumn(DynamicTableColumn column) { @@ -264,11 +264,12 @@ public abstract class GDynamicColumnTableModel } /** - * Adds the given columns to the end of the list of columns. This method is intended for + * Adds the given columns to the end of the list of columns. This method is intended for * implementations to add custom column objects, rather than relying on generic, discovered * DynamicTableColumn implementations. * - *

Note: this method assumes that the columns have already been sorted. + *

+ * Note: this method assumes that the columns have already been sorted. * * @param columns The columns to add */ @@ -280,15 +281,16 @@ public abstract class GDynamicColumnTableModel } /** - * Adds the given field at the given index to the list of fields in this class. - * This method is intended for implementations to add custom column objects, rather than - * relying on generic, discovered DynamicTableColumn implementations. + * Adds the given field at the given index to the list of fields in this class. This method is + * intended for implementations to add custom column objects, rather than relying on generic, + * discovered DynamicTableColumn implementations. *

* Note: this method assumes that the columns have already been sorted. + * * @param column The field to add. - * @param index The index at which to add the field. If the index value is invalid (negative - * or greater than the number of columns), then the column will be added to the - * end of the columns list. + * @param index The index at which to add the field. If the index value is invalid (negative or + * greater than the number of columns), then the column will be added to the end of + * the columns list. * @param isDefault true if this is a default column */ protected void addTableColumn(DynamicTableColumn column, int index, @@ -324,8 +326,8 @@ public abstract class GDynamicColumnTableModel } /** - * Removes the given columns from this model. This method allows the client to remove - * multiple columns at once, firing only one event when the work is finished. + * Removes the given columns from this model. This method allows the client to remove multiple + * columns at once, firing only one event when the work is finished. * * @param columns the columns to remove */ @@ -363,6 +365,7 @@ public abstract class GDynamicColumnTableModel /** * Returns true if the column indicated by the index in the model is a default column (meaning * that it was specified by the model and not discovered). + * * @param modelIndex the index of the column in the model. * @return true if the column is a default. */ @@ -460,7 +463,8 @@ public abstract class GDynamicColumnTableModel /** * Returns the table's context for the data. - * @return the table's context for the data. + * + * @return the table's context for the data. */ public abstract DATA_SOURCE getDataSource(); @@ -524,12 +528,12 @@ public abstract class GDynamicColumnTableModel } /** - * Gets the special table cell renderer for the specified table field column. - * A null value indicates that this field uses a default cell renderer. + * Gets the special table cell renderer for the specified table field column. A null value + * indicates that this field uses a default cell renderer. * * @param index the model column index - * @return a table cell renderer for this field. Otherwise, null if a default - * renderer should be used. + * @return a table cell renderer for this field. Otherwise, null if a default renderer should be + * used. */ @Override public TableCellRenderer getRenderer(int index) { @@ -537,8 +541,9 @@ public abstract class GDynamicColumnTableModel } /** - * Gets the maximum number of text display lines needed for any given cell within the - * specified column. + * Gets the maximum number of text display lines needed for any given cell within the specified + * column. + * * @param index column field index * @return maximum number of lines needed for specified column */ diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/MappedTableColumn.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/MappedTableColumn.java index 068fdad0fe..1983fca094 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/MappedTableColumn.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/MappedTableColumn.java @@ -23,15 +23,15 @@ import ghidra.framework.plugintool.ServiceProvider; import ghidra.util.table.column.GColumnRenderer; /** - * A class that is an Adapter in order to allow for the use of existing - * {@link DynamicTableColumn}s when the actual row type of the table is - * not the same as the row type that the {@link DynamicTableColumn} supports. + * A class that is an Adapter in order to allow for the use of existing {@link DynamicTableColumn}s + * when the actual row type of the table is not the same as the row type that the + * {@link DynamicTableColumn} supports. * * @param The table's actual row type * @param The row type expected by the given {@link DynamicTableColumn} * @param The column type provided by the given {@link DynamicTableColumn} - * @param the type of the data for each column; can be Object for columns that - * do not have a data source + * @param the type of the data for each column; can be Object for columns that do not + * have a data source */ public class MappedTableColumn extends AbstractDynamicTableColumn { @@ -110,8 +110,9 @@ public class MappedTableColumn getComparator() { - return tableColumn.getComparator(); + public Comparator getComparator(DynamicColumnTableModel model, + int columnIndex) { + return tableColumn.getComparator(model, columnIndex); } @Override