GP-1969: Add 'Model' provider for inspecting object-based traces.

This commit is contained in:
Dan 2022-06-15 15:41:38 -04:00
parent eb0a23aecc
commit 2a4b4f9bcf
59 changed files with 6131 additions and 269 deletions

View File

@ -100,7 +100,7 @@
and Service</A>. <B>Note:</B> Only the raw "Value" column can be edited directly. The "Repr"
column cannot be edited, yet.</P>
<H3><A name="snapshot_window"></A>Snapshot Window</H3>
<H3><A name="clone_window"></A>Clone Window</H3>
<P>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

View File

@ -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() {

View File

@ -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<P extends SaveableProvider> {
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<P> getDisconnectedProviders();
protected abstract P createDisconnectedProvider();
protected abstract void removeDisconnectedProvider(P p);
protected void doWrite(SaveState saveState, BiConsumer<? super P, ? super SaveState> writer) {
P cp = getConnectedProvider();
SaveState cpState = new SaveState();
writer.accept(cp, cpState);
saveState.putXmlElement(KEY_CONNECTED_PROVIDER, cpState.saveToXml());
List<P> disconnectedProviders = getDisconnectedProviders();
List<P> 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<? super P, ? super SaveState> 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<P> 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);
}
}

View File

@ -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<T> extends ThreadedTableModel<T, Trace>
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<Long> oldSpan,
Range<Long> 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<Long> 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
*
* <p>
* 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
*
* <p>
* 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<Long> span) {
if (Objects.equals(this.span, span)) {
return;
}
this.span = span;
spanChanged();
}
public Range<Long> 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<T> streamRows(Trace trace, ModelQuery query, Range<Long> span);
@Override
protected void doLoad(Accumulator<T> accumulator, TaskMonitor monitor)
throws CancelledException {
if (trace == null || query == null || trace.getObjectManager().getRootSchema() == null) {
return;
}
for (T t : (Iterable<T>) 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);
}

View File

@ -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<T> extends JPanel {
protected final AbstractQueryTableModel<T> tableModel;
protected final GhidraTable table;
private final GhidraTableFilterPanel<T> 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<T> 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<T> 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);
}
}

View File

@ -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<P extends JComponent> {
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<JTable> {
@Override
default Color getForeground(JTable table) {
return table.getForeground();
}
@Override
default Color getSelForeground(JTable table) {
return table.getSelectionForeground();
}
}
interface InTree extends ColorsModified<JTree>, TreeCellRenderer {
Color getTextNonSelectionColor();
Color getTextSelectionColor();
@Override
default Color getForeground(JTree tree) {
return getTextNonSelectionColor();
}
@Override
default Color getSelForeground(JTree tree) {
return getTextSelectionColor();
}
}
}

View File

@ -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<DebuggerModelProvider> {
@Override
protected DebuggerModelProvider getConnectedProvider() {
return connectedProvider;
}
@Override
protected List<DebuggerModelProvider> 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<DebuggerModelProvider> 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<DebuggerModelProvider> 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);
}
}

View File

@ -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<DebuggerModelProvider> 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<AbstractNode> 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<ValueRow> 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<PathRow> 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<TraceObjectValue> 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<TraceObjectValue> 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, "")));
}
}

View File

@ -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<TraceObjectValue> objectValues;
public DebuggerObjectActionContext(Collection<TraceObjectValue> objectValues,
ComponentProvider provider, Component sourceComponent) {
super(provider, sourceComponent);
this.objectValues = List.copyOf(objectValues);
}
public List<TraceObjectValue> getObjectValues() {
return objectValues;
}
}

View File

@ -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
*
* <p>
* 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
*
* <p>
* 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
*
* <p>
* 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);
}
}

View File

@ -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 "<root>";
}
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 "<html>" + HTMLUtilities.escapeHTML(getPrimitiveValueDisplay(edge.getValue()));
}
if (edge.isCanonical()) {
return "<html>" + HTMLUtilities.escapeHTML(getObjectDisplay(edge));
}
return "<html><em>" + HTMLUtilities.escapeHTML(getObjectLinkDisplay(edge)) + "</em>";
}
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);
}
}

View File

@ -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 "<ModelQuery: " + predicates.toString() + ">";
}
@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<TraceObject> streamObjects(Trace trace, Range<Long> 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<TraceObjectValue> streamValues(Trace trace, Range<Long> 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<TraceObjectValPath> streamPaths(Trace trace, Range<Long> span) {
return trace.getObjectManager().getValuePaths(span, predicates).map(p -> p);
}
/**
* Compute the named attributes for resulting objects, according to the schema
*
* <p>
* This does not include the "default attribute schema."
*
* @param trace the data source
* @return the list of attributes
*/
public Stream<AttributeSchema> 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
*
* <p>
* 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<Long> span, TraceObjectValue value) {
List<String> 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());
}
}

View File

@ -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<ValueRow> {
/** Initialized in {@link #createTableColumnDescriptor()}, which precedes this. */
private TraceValueValColumn valueColumn;
private TraceValueLifePlotColumn lifePlotColumn;
protected static Stream<? extends TraceObjectValue> distinctCanonical(
Stream<? extends TraceObjectValue> stream) {
Set<TraceObject> seen = new HashSet<>();
return stream.filter(value -> {
if (!value.isCanonical()) {
return true;
}
return seen.add(value.getChild());
});
}
public interface ValueRow {
String getKey();
RangeSet<Long> 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<Long> getLife() {
RangeSet<Long> 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 "<html>" +
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<ColKey, TraceValueObjectAttributeColumn> 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<Long> fullRange = Range.closed(0L, max == null ? 1 : max + 1);
lifePlotColumn.setFullRange(fullRange);
}
protected List<AttributeSchema> 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<AttributeSchema> 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<DynamicTableColumn<ValueRow, ?, ?>> computeAttributeColumns(
Collection<AttributeSchema> 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<AttributeSchema> attributes) {
Set<DynamicTableColumn<ValueRow, ?, ?>> columns =
new HashSet<>(computeAttributeColumns(attributes));
Set<DynamicTableColumn<ValueRow, ?, ?>> toRemove = new HashSet<>();
for (int i = 0; i < getColumnCount(); i++) {
DynamicTableColumn<ValueRow, ?, ?> exists = getColumn(i);
if (!(exists instanceof TraceValueObjectAttributeColumn)) {
continue;
}
if (!columns.remove(exists)) {
toRemove.add(exists);
}
}
removeTableColumns(toRemove);
addTableColumns(columns);
}
@Override
protected Stream<ValueRow> streamRows(Trace trace, ModelQuery query, Range<Long> span) {
return distinctCanonical(query.streamValues(trace, span)
.filter(v -> isShowHidden() || !v.isHidden()))
.map(this::rowForValue);
}
@Override
protected TableColumnDescriptor<ValueRow> createTableColumnDescriptor() {
TableColumnDescriptor<ValueRow> 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);
}
}
}

View File

@ -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<Class<? extends TargetObject>> 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<Long> oldSpan,
Range<Long> 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<IDKeyed<TraceObjectValue>, AbstractNode> byValue = new WeakValueHashMap<>();
Map<IDKeyed<TraceObject>, 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 "<html><em>No trace is active</em>";
}
TraceObject root = trace.getObjectManager().getRootObject();
if (root == null) {
return "<html><em>Trace has no model</em>";
}
return "<html>" +
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<GTreeNode> 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<GTreeNode> generateChildren() {
return List.of();
}
@Override
public String getName() {
String html = HTMLUtilities.escapeHTML(
value.getEntryKey() + ": " + display.getPrimitiveValueDisplay(value.getValue()));
return "<html>" + 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 "<html>" + HTMLUtilities.escapeHTML(value.getEntryKey()) + ": <em>" +
HTMLUtilities.escapeHTML(display.getObjectLinkDisplay(value)) + "</em>";
}
@Override
public String getToolTip() {
return display.getObjectLinkToolTip(value);
}
@Override
public boolean isLeaf() {
return true;
}
@Override
protected List<GTreeNode> 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<GTreeNode> generateChildren() {
return generateObjectChildren(object);
}
@Override
public String getName() {
return "<html>" + 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<Long> 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<String, Icon> 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<String, Icon> fillIconMap(Map<String, Icon> 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<GTreeNode> generateObjectChildren(TraceObject object) {
List<GTreeNode> 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
*
* <p>
* 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
*
* <p>
* 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<Long> span) {
if (Objects.equals(this.span, span)) {
return;
}
this.span = span;
spanChanged();
}
public Range<Long> 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);
}
}

View File

@ -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<ValueRow> {
public ObjectsTablePanel(Plugin plugin) {
super(plugin);
}
@Override
protected AbstractQueryTableModel<ValueRow> createModel(Plugin plugin) {
return new ObjectTableModel(plugin);
}
}

View File

@ -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, A> R getItemsFromPaths(TreePath[] paths,
Collector<? super AbstractNode, A, R> 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<AbstractNode> 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<TraceObjectKeyPath> keyPaths) {
List<GTreeNode> nodes = new ArrayList<>();
for (TraceObjectKeyPath path : keyPaths) {
AbstractNode node = getNode(path);
if (node != null) {
nodes.add(node);
}
}
tree.setSelectedNodes(nodes);
}
}

View File

@ -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<PathRow> {
/** Initialized in {@link #createTableColumnDescriptor()}, which precedes this. */
private TracePathValueColumn valueColumn;
private TracePathLastLifespanPlotColumn lifespanPlotColumn;
protected static Stream<? extends TraceObjectValPath> distinctKeyPath(
Stream<? extends TraceObjectValPath> stream) {
Set<List<String>> 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<Long> 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<PathRow> streamRows(Trace trace, ModelQuery query, Range<Long> 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<PathRow> createTableColumnDescriptor() {
TableColumnDescriptor<PathRow> 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);
}
}

View File

@ -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<PathRow> {
public PathsTablePanel(Plugin plugin) {
super(plugin);
}
@Override
protected AbstractQueryTableModel<PathRow> createModel(Plugin plugin) {
return new PathTableModel(plugin);
}
}

View File

@ -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<PathRow, String, Trace> {
@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 "<root>";
}
return lastEntry.getEntryKey();
}
}

View File

@ -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<PathRow, Range<Long>, Trace> {
@Override
public String getColumnName() {
return "Life";
}
@Override
public Range<Long> 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();
}
}

View File

@ -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<PathRow, Range<Long>, Trace> {
private final RangeTableCellRenderer<Long> cellRenderer = new RangeTableCellRenderer<>();
@Override
public String getColumnName() {
return "Plot";
}
@Override
public Range<Long> 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<Range<Long>> getColumnRenderer() {
return cellRenderer;
}
// TODO: header renderer
public void setFullRange(Range<Long> fullRange) {
cellRenderer.setFullRange(fullRange);
// TODO: header, too
}
}

View File

@ -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<PathRow, String, Trace> {
@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());
}
}

View File

@ -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<PathRow, PathRow, Trace> {
private final class ValueRenderer extends AbstractGColumnRenderer<PathRow>
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<PathRow> getColumnRenderer() {
return new ValueRenderer();
}
public void setDiffColor(Color diffColor) {
this.diffColor = diffColor;
}
public void setDiffColorSel(Color diffColorSel) {
this.diffColorSel = diffColorSel;
}
}

View File

@ -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<ValueRow, String, Trace> {
@Override
public String getColumnName() {
return "Key";
}
@Override
public String getValue(ValueRow rowObject, Settings settings, Trace data,
ServiceProvider serviceProvider) throws IllegalArgumentException {
return rowObject.getKey();
}
}

View File

@ -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<ValueRow, RangeSet<Long>, Trace> {
@Override
public String getColumnName() {
return "Life";
}
@Override
public RangeSet<Long> getValue(ValueRow rowObject, Settings settings, Trace data,
ServiceProvider serviceProvider) throws IllegalArgumentException {
return rowObject.getLife();
}
}

View File

@ -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<ValueRow, RangeSet<Long>, Trace> {
private final RangeSetTableCellRenderer<Long> cellRenderer = new RangeSetTableCellRenderer<>();
@Override
public String getColumnName() {
return "Plot";
}
@Override
public RangeSet<Long> getValue(ValueRow rowObject, Settings settings, Trace data,
ServiceProvider serviceProvider) throws IllegalArgumentException {
return rowObject.getLife();
}
@Override
public GColumnRenderer<RangeSet<Long>> getColumnRenderer() {
return cellRenderer;
}
// TODO: The header renderer
public void setFullRange(Range<Long> fullRange) {
cellRenderer.setFullRange(fullRange);
// TODO: set header's full range, too
}
}

View File

@ -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<ValueRow, ValueRow, Trace> {
public class AttributeRenderer extends AbstractGColumnRenderer<ValueRow>
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<ValueRow> 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<ValueRow> getColumnRenderer() {
return renderer;
}
@Override
public Comparator<ValueRow> 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 <C extends Comparable<C>> Comparator<ValueRow> newTypedComparator() {
if (Comparable.class.isAssignableFrom(attributeType)) {
@SuppressWarnings("unchecked")
Class<C> cls = (Class<C>) attributeType.asSubclass(Comparable.class);
Function<ValueRow, C> 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;
}
}

View File

@ -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<ValueRow, ValueRow, Trace> {
private final class ValRenderer extends AbstractGColumnRenderer<ValueRow>
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<ValueRow> getColumnRenderer() {
return renderer;
}
@Override
public Comparator<ValueRow> getComparator(DynamicColumnTableModel<?> model, int columnIndex) {
return getComparator()
.thenComparing(new ColumnRenderedValueBackupComparator<>(model, columnIndex));
}
@Override
@SuppressWarnings("unchecked")
public Comparator<ValueRow> 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<Object>) 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;
}
}

View File

@ -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<Register> collectCommonRegisters(CompilerSpec cSpec) {
Language lang = cSpec.getLanguage();
LinkedHashSet<Register> 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;

View File

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

View File

@ -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<String> result = new LinkedHashSet<>(object.getInterfaceNames());
for (Class<? extends TargetObject> 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) {

View File

@ -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("" + //
"<context>" + //
" <schema name='Session' elementResync='NEVER' attributeResync='ONCE'>" + //
" <attribute name='Processes' schema='ProcessContainer' />" + //
" <interface name='EventScope' />" + //
" </schema>" + //
" <schema name='ProcessContainer' canonical='yes' elementResync='NEVER' " + //
" attributeResync='ONCE'>" + //
" <element schema='Process' />" + //
" </schema>" + //
" <schema name='Process' elementResync='NEVER' attributeResync='ONCE'>" + //
" <attribute name='Threads' schema='ThreadContainer' />" + //
" <attribute name='Handles' schema='HandleContainer' />" + //
" </schema>" + //
" <schema name='ThreadContainer' canonical='yes' elementResync='NEVER' " + //
" attributeResync='ONCE'>" + //
" <element schema='Thread' />" + //
" </schema>" + //
" <schema name='Thread' elementResync='NEVER' attributeResync='NEVER'>" + //
" <interface name='Thread' />" + //
" <attribute name='_display' schema='STRING' />" + //
" <attribute name='_self' schema='Thread' />" + //
" </schema>" + //
" <schema name='HandleContainer' canonical='yes' elementResync='NEVER' " + //
" attributeResync='ONCE'>" + //
" <element schema='INT' />" + //
" </schema>" + //
"</context>");
}
catch (JDOMException e) {
throw new AssertionError();
}
}
protected static Integer findColumnOfClass(GDynamicColumnTableModel<?, ?> model,
Class<? extends DynamicTableColumn<?, ?, ?>> 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<ValueRow> 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("<html>[2]", node.getDisplayText());
try (UndoableTransaction tid = tb.startTransaction()) {
thread.setAttribute(Range.atLeast(0L), "_display", "Renamed Thread");
}
waitForSwing();
waitForPass(() -> assertEquals("<html>Renamed Thread", node.getDisplayText()));
}
}

View File

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

View File

@ -414,7 +414,8 @@ public interface TargetObjectSchema {
*
* <p>
* 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

View File

@ -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<String> path) {
return true;
}
@Override
public boolean successorCouldMatch(List<String> path, boolean strict) {
return true;
}
@Override
public boolean ancestorMatches(List<String> path, boolean strict) {
if (path.isEmpty() && strict) {
return false;
}
return true;
}
@Override
public Set<String> getNextKeys(List<String> path) {
return Set.of("", "[]");
}
@Override
public Set<String> getNextNames(List<String> path) {
return Set.of("");
}
@Override
public Set<String> getNextIndices(List<String> path) {
return Set.of("");
}
@Override
public List<String> getSingletonPath() {
return null;
}
@Override
public PathPattern getSingletonPattern() {
return null;
}
@Override
public PathPredicates applyKeys(List<String> keys) {
throw new UnsupportedOperationException();
}
@Override
public boolean isEmpty() {
return false;
}
}

View File

@ -38,6 +38,21 @@ public class PathMatcher implements PathPredicates {
return String.format("<PathMatcher\n %s\n>", 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<String> path, boolean strict) {
return anyPattern(p -> p.ancestorCouldMatchRight(path, strict));
}
@Override
public List<String> getSingletonPath() {
if (patterns.size() != 1) {
@ -98,12 +118,7 @@ public class PathMatcher implements PathPredicates {
return patterns.iterator().next();
}
@Override
public Set<String> getNextKeys(List<String> path) {
Set<String> result = new HashSet<>();
for (PathPattern pattern : patterns) {
result.addAll(pattern.getNextKeys(path));
}
protected void coalesceWilds(Set<String> 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<String> getNextKeys(List<String> path) {
Set<String> 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<String> getPrevKeys(List<String> path) {
Set<String> 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;
}
}

View File

@ -48,13 +48,25 @@ public class PathPattern implements PathPredicates {
return String.format("<PathPattern %s>", 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> getPrevKeys(List<String> 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;
}
}

View File

@ -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<String> path, boolean strict);
/**
* Check if the given path <em>could</em> have a matching ancestor, right to left
*
* <p>
* 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<String> path, boolean strict);
/**
* Get the patterns for the next possible key
*
@ -130,6 +138,17 @@ public interface PathPredicates {
*/
Set<String> getNextIndices(List<String> path);
/**
* Get the patterns for the previous possible key (right-to-left matching)
*
* <p>
* 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<String> getPrevKeys(List<String> 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<List<String>, ?> getCachedValues(TargetObject seed) {
return getCachedValues(List.of(), seed);
}

View File

@ -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("[]")));
}
}

View File

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

View File

@ -203,6 +203,7 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject {
@Override
public RangeSet<Long> getLife() {
// TODO: This should really be cached
try (LockHold hold = manager.trace.lockRead()) {
RangeSet<Long> 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<Long> lifespan, ConflictResolution resolution) {
protected DBTraceObjectValPath doInsert(Range<Long> 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<Long> lifespan, ConflictResolution resolution) {
public DBTraceObjectValPath insert(Range<Long> 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<Long> 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<Long> 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<? extends DBTraceObjectValue> doGetParents() {
return manager.valuesByChild.get(this);
}
@Override
public Collection<? extends DBTraceObjectValue> 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<? extends DBTraceObjectValPath> getAncestors(
public Stream<? extends TraceObjectValPath> getAncestors(Range<Long> span,
PathPredicates relativePredicates) {
try (LockHold hold = manager.trace.lockRead()) {
Stream<? extends DBTraceObjectValPath> ancestors =
doStreamVisitor(span, new InternalAncestorsRelativeVisitor(relativePredicates));
if (relativePredicates.matches(List.of())) {
return Stream.concat(Stream.of(DBTraceObjectValPath.of()), ancestors);
}
return ancestors;
}
}
@Override
public Stream<? extends DBTraceObjectValPath> getAncestorsRoot(
Range<Long> span, PathPredicates rootPredicates) {
try (LockHold hold = manager.trace.lockRead()) {
return doStreamVisitor(span, new InternalAncestorsVisitor(rootPredicates));
return doStreamVisitor(span, new InternalAncestorsRootVisitor(rootPredicates));
}
}
@Override
public Stream<? extends DBTraceObjectValPath> getSuccessors(
Range<Long> span, PathPredicates relativePredicates) {
DBTraceObjectValPath empty = DBTraceObjectValPath.of();
try (LockHold hold = manager.trace.lockRead()) {
Stream<? extends DBTraceObjectValPath> 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<? extends TargetObject> 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

View File

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

View File

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

View File

@ -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

View File

@ -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<String> 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<? extends InternalTraceObjectValue> continueValues(DBTraceObject object,
Range<Long> span, DBTraceObjectValPath pre) {
Set<String> prevKeys = predicates.getPrevKeys(pre.getKeyList());
if (prevKeys.isEmpty()) {
return Stream.empty();
}
return object.doGetParents()
.stream()
.filter(v -> PathPredicates.anyMatches(prevKeys, v.getEntryKey()));
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 <T> the type of the affected object
* @param <U> the type of the values
* @param type the event type
* @param handler the handler
*/
protected <T, U> void listenFor(TraceChangeType<T, U> type,
AffectedAndValuesOnlyHandler<? super T, ? super U> handler) {
typedMap.put(type, handler);

View File

@ -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<Long> lifespan, ConflictResolution resolution);
TraceObjectValPath insert(Range<Long> 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
*
* <p>
* 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<? extends TraceObjectValPath> getAncestors(Range<Long> span,
Stream<? extends TraceObjectValPath> getAncestorsRoot(Range<Long> 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<? extends TraceObjectValPath> getAncestors(Range<Long> 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;
}
}

View File

@ -225,9 +225,26 @@ public final class TraceObjectKeyPath implements Comparable<TraceObjectKeyPath>
if (!predicates.ancestorMatches(keyList, false)) {
return Stream.of();
}
Stream<TraceObjectKeyPath> 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
*
* <p>
* 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);
}
}

View File

@ -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
*
* <p>
* 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<Long> 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());
}
}

View File

@ -26,21 +26,24 @@ import javax.swing.table.*;
import com.google.common.collect.Range;
public class RangeCursorTableHeaderRenderer<N extends Number & Comparable<N>>
extends GTableHeaderRenderer {
extends GTableHeaderRenderer implements RangedRenderer<N> {
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<Double> fullRange = Range.closed(0d, 1d);
protected Range<Double> fullRangeDouble = Range.closed(0d, 1d);
protected double span = 1;
protected Range<N> fullRange;
protected N pos;
protected double doublePos;
@Override
public void setFullRange(Range<N> 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<N extends Number & Comparable<N>>
@Override
protected void paintChildren(Graphics g) {
super.paintChildren(g);
// The cursor should occlude the children
paintCursor(g);
}
@ -58,7 +62,7 @@ public class RangeCursorTableHeaderRenderer<N extends Number & Comparable<N>>
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<N extends Number & Comparable<N>>
}
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<N extends Number & Comparable<N>>
public N getCursorPosition() {
return pos;
}
@Override
public Range<N> getFullRange() {
return fullRange;
}
@Override
public Range<Double> getFullRangeDouble() {
return fullRangeDouble;
}
@Override
public double getSpan() {
return span;
}
}

View File

@ -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<N extends Number & Comparable<N>>
extends AbstractGColumnRenderer<RangeSet<N>> implements RangedRenderer<N> {
protected Range<Double> fullRangeDouble = Range.closed(0d, 1d);
protected double span = 1;
protected Range<N> fullRange;
protected RangeSet<N> dataRangeSet;
@Override
public void setFullRange(Range<N> fullRange) {
this.fullRange = fullRange;
this.fullRangeDouble = RangedRenderer.validateViewRange(fullRange);
this.span = this.fullRangeDouble.upperEndpoint() - this.fullRangeDouble.lowerEndpoint();
}
@Override
public String getFilterString(RangeSet<N> t, Settings settings) {
return "";
}
@Override
@SuppressWarnings("unchecked")
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
this.dataRangeSet = (RangeSet<N>) 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<N> range : dataRangeSet.asRanges()) {
paintRange(g, range);
}
}
@Override
public Range<N> getFullRange() {
return fullRange;
}
@Override
public Range<Double> getFullRangeDouble() {
return fullRangeDouble;
}
@Override
public double getSpan() {
return span;
}
}

View File

@ -24,27 +24,19 @@ import ghidra.docking.settings.Settings;
import ghidra.util.table.column.AbstractGColumnRenderer;
public class RangeTableCellRenderer<N extends Number & Comparable<N>>
extends AbstractGColumnRenderer<Range<N>> {
extends AbstractGColumnRenderer<Range<N>> implements RangedRenderer<N> {
protected Range<Double> doubleFullRange = Range.closed(0d, 1d);
protected Range<Double> fullRangeDouble = Range.closed(0d, 1d);
protected double span = 1;
protected Range<N> fullRange;
protected Range<N> dataRange;
public static Range<Double> validateViewRange(Range<? extends Number> 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<N> 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<N extends Number & Comparable<N>>
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<N> getFullRange() {
return fullRange;
}
@Override
public Range<Double> getFullRangeDouble() {
return fullRangeDouble;
}
@Override
public double getSpan() {
return span;
}
}

View File

@ -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<N extends Number & Comparable<N>> {
public static Range<Double> validateViewRange(Range<? extends Number> 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<N> fullRange);
Range<N> getFullRange();
Range<Double> getFullRangeDouble();
double getSpan();
default int interpolate(int w, double val) {
Range<Double> 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<N> 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);
}
}

View File

@ -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 <ROW_TYPE> The row object class supported by this column
* @param <COLUMN_TYPE> The column object class supported by this column
* @param <DATA_SOURCE> The object class type that will be passed to
* see <code>getValue(ROW_TYPE, Settings, DATA_SOURCE, ServiceProvider)</code>
* @param <DATA_SOURCE> The object class type that will be passed to see
* <code>getValue(ROW_TYPE, Settings, DATA_SOURCE, ServiceProvider)</code>
*/
public abstract class AbstractDynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE>
implements DynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE> {
@ -80,11 +79,16 @@ public abstract class AbstractDynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOU
return -1;
}
@Override
public Comparator<COLUMN_TYPE> getComparator() {
return null;
}
@Override
public Comparator<COLUMN_TYPE> getComparator(DynamicColumnTableModel<?> model,
int columnIndex) {
return getComparator();
}
@Override
@SuppressWarnings("unchecked")
// enforced by the compiler

View File

@ -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 <ROW_TYPE> The row object class supported by this column
* @param <COLUMN_TYPE> The column object class supported by this column
* @param <DATA_SOURCE> The object class type that will be passed to
* see <code>getValue(ROW_TYPE, Settings, DATA_SOURCE, ServiceProvider)</code>
* @param <DATA_SOURCE> The object class type that will be passed to see
* <code>getValue(ROW_TYPE, Settings, DATA_SOURCE, ServiceProvider)</code>
*/
public interface DynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE> {
/**
* 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<COLUMN_TYPE> 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<ROW_TYPE> 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<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE> {
/**
* Returns the optional cell renderer for this column; null if no renderer is used.
*
* <P>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
* <P>
* 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).
*
* <P>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}.
* <P>
* 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<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE> {
/**
* 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<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE> {
/**
* 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 <code>null</code> if they do not wish to provider a comparator
*
* @return the comparator
*/
public Comparator<COLUMN_TYPE> getComparator();
public Comparator<COLUMN_TYPE> getComparator(DynamicColumnTableModel<?> model,
int columnIndex);
}

View File

@ -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.
* <p>
* This model will also discover other system columns that understand how to render
* <code>ROW_TYPE</code> data directly. Also, if you create a {@link TableRowMapper mapper}(s) for
* <code>ROW_TYPE</code> 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.
* <p>
* 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.
* <p>
* 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 <ROW_TYPE> the row object class for this table model.
* @param <DATA_SOURCE> 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 <DATA_SOURCE> 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<ROW_TYPE, DATA_SOURCE>
extends AbstractSortedTableModel<ROW_TYPE>
@ -122,10 +120,10 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
}
/**
* 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<ROW_TYPE, DATA_SOURCE>
}
/**
* 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<ROW_TYPE, DATA_SOURCE>
@SuppressWarnings("unchecked") // the column provides the values itself; safe cast
protected Comparator<Object> createSortComparatorForColumn(int columnIndex) {
DynamicTableColumn<ROW_TYPE, ?, ?> column = getColumn(columnIndex);
Comparator<Object> comparator = (Comparator<Object>) column.getComparator();
Comparator<Object> comparator =
(Comparator<Object>) column.getComparator(this, columnIndex);
return comparator;
}
@ -252,11 +250,13 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
}
/**
* 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.
*
* <p><b>Note: this method assumes that the columns have already been sorted</b>
* <p>
* <b>Note: this method assumes that the columns have already been sorted</b>
*
* @param column The field to add
*/
protected void addTableColumn(DynamicTableColumn<ROW_TYPE, ?, ?> column) {
@ -264,11 +264,12 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
}
/**
* 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.
*
* <p><b>Note: this method assumes that the columns have already been sorted.</b>
* <p>
* <b>Note: this method assumes that the columns have already been sorted.</b>
*
* @param columns The columns to add
*/
@ -280,15 +281,16 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
}
/**
* 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.
* <p>
* <b>Note: this method assumes that the columns have already been sorted.</b>
*
* @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<ROW_TYPE, ?, ?> column, int index,
@ -324,8 +326,8 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
}
/**
* 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<ROW_TYPE, DATA_SOURCE>
/**
* 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<ROW_TYPE, DATA_SOURCE>
/**
* 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<ROW_TYPE, DATA_SOURCE>
}
/**
* 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<ROW_TYPE, DATA_SOURCE>
}
/**
* 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
*/

View File

@ -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 <ROW_TYPE> The table's actual row type
* @param <EXPECTED_ROW_TYPE> The row type expected by the given {@link DynamicTableColumn}
* @param <COLUMN_TYPE> The column type provided by the given {@link DynamicTableColumn}
* @param <DATA_SOURCE> the type of the data for each column; can be Object for columns that
* do not have a data source
* @param <DATA_SOURCE> the type of the data for each column; can be Object for columns that do not
* have a data source
*/
public class MappedTableColumn<ROW_TYPE, EXPECTED_ROW_TYPE, COLUMN_TYPE, DATA_SOURCE>
extends AbstractDynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE> {
@ -110,8 +110,9 @@ public class MappedTableColumn<ROW_TYPE, EXPECTED_ROW_TYPE, COLUMN_TYPE, DATA_SO
}
@Override
public Comparator<COLUMN_TYPE> getComparator() {
return tableColumn.getComparator();
public Comparator<COLUMN_TYPE> getComparator(DynamicColumnTableModel<?> model,
int columnIndex) {
return tableColumn.getComparator(model, columnIndex);
}
@Override