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" and Service</A>. <B>Note:</B> Only the raw "Value" column can be edited directly. The "Repr"
column cannot be edited, yet.</P> 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 <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 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.listing.DebuggerListingPlugin;
import ghidra.app.plugin.core.debug.gui.memory.DebuggerMemoryBytesPlugin; 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.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.DebuggerModulesPlugin;
import ghidra.app.plugin.core.debug.gui.modules.DebuggerStaticMappingPlugin; import ghidra.app.plugin.core.debug.gui.modules.DebuggerStaticMappingPlugin;
import ghidra.app.plugin.core.debug.gui.objects.DebuggerObjectsPlugin; 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_SELECT_ROWS = ResourceManager.loadImage("images/table_go.png");
ImageIcon ICON_AUTOREAD = ResourceManager.loadImage("images/autoread.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. // TODO: Draw a real icon.
ImageIcon ICON_REFRESH_MEMORY = ICON_REFRESH; ImageIcon ICON_REFRESH_MEMORY = ICON_REFRESH;
@ -255,6 +259,11 @@ public interface DebuggerResources {
HelpLocation HELP_PROVIDER_OBJECTS = new HelpLocation( HelpLocation HELP_PROVIDER_OBJECTS = new HelpLocation(
PluginUtils.getPluginNameFromClass(DebuggerObjectsPlugin.class), HELP_ANCHOR_PLUGIN); 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"; String TITLE_PROVIDER_WATCHES = "Watches";
ImageIcon ICON_PROVIDER_WATCHES = ICON_AUTOREAD; // TODO: Another icon? ImageIcon ICON_PROVIDER_WATCHES = ICON_AUTOREAD; // TODO: Another icon?
HelpLocation HELP_PROVIDER_WATCHES = new HelpLocation( 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); 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_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"; String OPTION_NAME_COLORS_REGISTER_STALE = "Colors.Stale Registers";
Color DEFAULT_COLOR_REGISTER_STALE = Color.GRAY; Color DEFAULT_COLOR_REGISTER_STALE = Color.GRAY;
String OPTION_NAME_COLORS_REGISTER_STALE_SEL = "Colors.Stale Registers (selected)"; 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)"; 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); 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"; String OPTION_NAME_COLORS_PCODE_COUNTER = "Colors.Pcode Counter";
Color DEFAULT_COLOR_PCODE_COUNTER = new Color(0.75f, 0.875f, 0.75f); Color DEFAULT_COLOR_PCODE_COUNTER = new Color(0.75f, 0.875f, 0.75f);
@ -994,12 +1011,12 @@ public interface DebuggerResources {
} }
} }
interface CreateSnapshotAction { interface CloneWindowAction {
String NAME = "Create Snapshot"; String NAME = "Clone Window";
String DESCRIPTION = "Create a (disconnected) snapshot copy of this window"; String DESCRIPTION = "Create a disconnected copy of this window";
String GROUP = "zzzz"; String GROUP = "zzzz";
Icon ICON = ResourceManager.loadImage("images/camera-photo.png"); Icon ICON = ResourceManager.loadImage("images/camera-photo.png");
String HELP_ANCHOR = "snapshot_window"; String HELP_ANCHOR = "clone_window";
static ActionBuilder builder(Plugin owner) { static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName(); 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 { abstract class AbstractStepSnapForwardAction extends DockingAction {
public static final String NAME = "Step Trace Snap Forward"; public static final String NAME = StepSnapForwardAction.NAME;
public static final Icon ICON = ICON_SNAP_FORWARD; public static final Icon ICON = StepSnapForwardAction.ICON;
public static final String HELP_ANCHOR = "step_trace_snap_forward"; public static final String HELP_ANCHOR = StepSnapForwardAction.HELP_ANCHOR;
public AbstractStepSnapForwardAction(Plugin owner) { public AbstractStepSnapForwardAction(Plugin owner) {
super(NAME, owner.getName()); super(NAME, owner.getName());
setDescription("Navigate the recording forward one snap"); setDescription(StepSnapForwardAction.DESCRIPTION);
setHelpLocation(new HelpLocation(owner.getName(), HELP_ANCHOR)); 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 { abstract class AbstractStepSnapBackwardAction extends DockingAction {
public static final String NAME = "Step Trace Snap Backward"; public static final String NAME = StepSnapBackwardAction.NAME;
public static final Icon ICON = ICON_SNAP_BACKWARD; public static final Icon ICON = StepSnapBackwardAction.ICON;;
public static final String HELP_ANCHOR = "step_trace_snap_backward"; public static final String HELP_ANCHOR = StepSnapBackwardAction.HELP_ANCHOR;
public AbstractStepSnapBackwardAction(Plugin owner) { public AbstractStepSnapBackwardAction(Plugin owner) {
super(NAME, owner.getName()); super(NAME, owner.getName());
setDescription("Navigate the recording backward one snap"); setDescription(StepSnapBackwardAction.DESCRIPTION);
setHelpLocation(new HelpLocation(owner.getName(), HELP_ANCHOR)); 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 { public abstract class AbstractDebuggerConnectionsNode extends GTreeNode {
@Override @Override
public String getName() { 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()) .onAction(c -> selectRegistersActivated())
.buildAndInstallLocal(this); .buildAndInstallLocal(this);
if (!isClone) { if (!isClone) {
actionCreateSnapshot = DebuggerResources.CreateSnapshotAction.builder(plugin) actionCreateSnapshot = DebuggerResources.CloneWindowAction.builder(plugin)
.enabledWhen(c -> current.getThread() != null) .enabledWhen(c -> current.getThread() != null)
.onAction(c -> createSnapshotActivated()) .onAction(c -> cloneWindowActivated())
.buildAndInstallLocal(this); .buildAndInstallLocal(this);
} }
actionEnableEdits = DebuggerResources.EnableEditsAction.builder(plugin) actionEnableEdits = DebuggerResources.EnableEditsAction.builder(plugin)
@ -639,7 +639,7 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter
tool.showDialog(availableRegsDialog); tool.showDialog(availableRegsDialog);
} }
private void createSnapshotActivated() { private void cloneWindowActivated() {
DebuggerRegistersProvider clone = cloneAsDisconnected(); DebuggerRegistersProvider clone = cloneAsDisconnected();
clone.setIntraGroupPosition(WindowPosition.RIGHT); clone.setIntraGroupPosition(WindowPosition.RIGHT);
tool.showComponentProvider(clone, true); tool.showComponentProvider(clone, true);
@ -961,8 +961,14 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter
public static LinkedHashSet<Register> collectCommonRegisters(CompilerSpec cSpec) { public static LinkedHashSet<Register> collectCommonRegisters(CompilerSpec cSpec) {
Language lang = cSpec.getLanguage(); Language lang = cSpec.getLanguage();
LinkedHashSet<Register> result = new LinkedHashSet<>(); LinkedHashSet<Register> result = new LinkedHashSet<>();
result.add(cSpec.getStackPointer()); Register sp = cSpec.getStackPointer();
result.add(lang.getProgramCounter()); if (sp != null) {
result.add(sp);
}
Register pc = lang.getProgramCounter();
if (pc != null) {
result.add(pc);
}
for (Register reg : lang.getRegisters()) { for (Register reg : lang.getRegisters()) {
//if (reg.getGroup() != null) { //if (reg.getGroup() != null) {
// continue; // continue;

View file

@ -392,6 +392,9 @@ public class ObjectBasedTraceRecorder implements TraceRecorder {
@Override @Override
public TargetThread getTargetThread(TraceThread thread) { public TargetThread getTargetThread(TraceThread thread) {
if (thread == null) {
return null;
}
return objectRecorder.getTargetInterface(thread, TraceObjectThread.class, return objectRecorder.getTargetInterface(thread, TraceObjectThread.class,
TargetThread.class); TargetThread.class);
} }

View file

@ -23,6 +23,7 @@ import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import com.google.common.collect.Range; import com.google.common.collect.Range;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.target.TargetAttacher.TargetAttachKind; import ghidra.dbg.target.TargetAttacher.TargetAttachKind;
import ghidra.dbg.target.TargetAttacher.TargetAttachKindSet; import ghidra.dbg.target.TargetAttacher.TargetAttachKindSet;
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
@ -75,6 +76,23 @@ class ObjectRecorder {
return targetObject == null ? null : targetObject.obj; 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) { protected void recordCreated(long snap, TargetObject object) {
TraceObject traceObject; TraceObject traceObject;
if (object.isRoot()) { if (object.isRoot()) {
@ -91,6 +109,10 @@ class ObjectRecorder {
Msg.error(this, "Received created for an object that already exists: " + exists); 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) { 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> * <p>
* If this is the schema of the root object, then this gives the schema of the object at the * 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 * @param path the relative path from an object having this schema to the desired successor
* @return the schema for the 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 ")); 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 @Override
public PathPredicates or(PathPredicates that) { public PathPredicates or(PathPredicates that) {
PathMatcher result = new PathMatcher(); PathMatcher result = new PathMatcher();
@ -82,6 +97,11 @@ public class PathMatcher implements PathPredicates {
return anyPattern(p -> p.ancestorMatches(path, strict)); return anyPattern(p -> p.ancestorMatches(path, strict));
} }
@Override
public boolean ancestorCouldMatchRight(List<String> path, boolean strict) {
return anyPattern(p -> p.ancestorCouldMatchRight(path, strict));
}
@Override @Override
public List<String> getSingletonPath() { public List<String> getSingletonPath() {
if (patterns.size() != 1) { if (patterns.size() != 1) {
@ -98,12 +118,7 @@ public class PathMatcher implements PathPredicates {
return patterns.iterator().next(); return patterns.iterator().next();
} }
@Override protected void coalesceWilds(Set<String> result) {
public Set<String> getNextKeys(List<String> path) {
Set<String> result = new HashSet<>();
for (PathPattern pattern : patterns) {
result.addAll(pattern.getNextKeys(path));
}
if (result.contains("")) { if (result.contains("")) {
result.removeIf(PathUtils::isName); result.removeIf(PathUtils::isName);
result.add(""); result.add("");
@ -112,6 +127,15 @@ public class PathMatcher implements PathPredicates {
result.removeIf(PathUtils::isIndex); result.removeIf(PathUtils::isIndex);
result.add("[]"); 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; return result;
} }
@ -139,6 +163,16 @@ public class PathMatcher implements PathPredicates {
return result; 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 @Override
public boolean isEmpty() { public boolean isEmpty() {
return patterns.isEmpty(); return patterns.isEmpty();
@ -152,4 +186,13 @@ public class PathMatcher implements PathPredicates {
} }
return result; 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)); 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 @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (!(obj instanceof PathPattern)) { if (!(obj instanceof PathPattern)) {
return false; return false;
} }
PathPattern that = (PathPattern) obj; PathPattern that = (PathPattern) obj;
return Objects.equals(this.pattern, that.pattern); if (!Objects.equals(this.pattern, that.pattern)) {
return false;
}
return true;
} }
@Override @Override
@ -95,6 +107,17 @@ public class PathPattern implements PathPredicates {
return true; 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 @Override
public boolean matches(List<String> path) { public boolean matches(List<String> path) {
if (path.size() != pattern.size()) { if (path.size() != pattern.size()) {
@ -125,6 +148,17 @@ public class PathPattern implements PathPredicates {
return matchesUpTo(path, pattern.size()); 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) { protected static boolean containsWildcards(List<String> pattern) {
for (String pat : pattern) { for (String pat : pattern) {
if (isWildcard(pat)) { if (isWildcard(pat)) {
@ -142,6 +176,20 @@ public class PathPattern implements PathPredicates {
return pattern; 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() { public int countWildcards() {
return (int) pattern.stream().filter(k -> isWildcard(k)).count(); return (int) pattern.stream().filter(k -> isWildcard(k)).count();
} }
@ -192,6 +240,17 @@ public class PathPattern implements PathPredicates {
return Set.of(); 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 @Override
public boolean isEmpty() { public boolean isEmpty() {
return false; return false;
@ -254,4 +313,18 @@ public class PathPattern implements PathPredicates {
} }
return result; 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)); return new PathPattern(PathUtils.parse(pattern));
} }
static PathPredicates all() {
return AllPathsMatcher.INSTANCE;
}
PathPredicates or(PathPredicates that); PathPredicates or(PathPredicates that);
/** /**
@ -95,6 +91,18 @@ public interface PathPredicates {
*/ */
boolean ancestorMatches(List<String> path, boolean strict); 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 * Get the patterns for the next possible key
* *
@ -130,6 +138,17 @@ public interface PathPredicates {
*/ */
Set<String> getNextIndices(List<String> path); 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 * 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(); 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) { default NavigableMap<List<String>, ?> getCachedValues(TargetObject seed) {
return getCachedValues(List.of(), 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); PathMatcher procMatcher = schema.searchFor(TargetProcess.class, false);
return object.getAncestors(getLifespan(), procMatcher) return object.getAncestorsRoot(getLifespan(), procMatcher)
.flatMap(proc -> proc.getSource(object) .flatMap(proc -> proc.getSource(object)
.querySuccessorsInterface(getLifespan(), .querySuccessorsInterface(getLifespan(),
TraceObjectThread.class)) TraceObjectThread.class))

View file

@ -203,6 +203,7 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject {
@Override @Override
public RangeSet<Long> getLife() { public RangeSet<Long> getLife() {
// TODO: This should really be cached
try (LockHold hold = manager.trace.lockRead()) { try (LockHold hold = manager.trace.lockRead()) {
RangeSet<Long> result = TreeRangeSet.create(); RangeSet<Long> result = TreeRangeSet.create();
// NOTE: connected ranges should already be coalesced // NOTE: connected ranges should already be coalesced
@ -220,19 +221,20 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject {
return manager.doGetObject(path.parent()); return manager.doGetObject(path.parent());
} }
protected void doInsert(Range<Long> lifespan, ConflictResolution resolution) { protected DBTraceObjectValPath doInsert(Range<Long> lifespan, ConflictResolution resolution) {
if (path.isRoot()) { if (path.isRoot()) {
return; return DBTraceObjectValPath.of();
} }
DBTraceObject parent = doCreateCanonicalParentObject(); DBTraceObject parent = doCreateCanonicalParentObject();
parent.setValue(lifespan, path.key(), this, resolution); InternalTraceObjectValue value = parent.setValue(lifespan, path.key(), this, resolution);
parent.doInsert(lifespan, resolution); DBTraceObjectValPath path = parent.doInsert(lifespan, resolution);
return path.append(value);
} }
@Override @Override
public void insert(Range<Long> lifespan, ConflictResolution resolution) { public DBTraceObjectValPath insert(Range<Long> lifespan, ConflictResolution resolution) {
try (LockHold hold = manager.trace.lockWrite()) { 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) { protected void doRemoveTree(Range<Long> span) {
for (DBTraceObjectValue parent : getParents()) {
parent.doTruncateOrDeleteAndEmitLifeChange(span);
}
for (InternalTraceObjectValue value : getValues()) { for (InternalTraceObjectValue value : getValues()) {
value.doTruncateOrDeleteAndEmitLifeChange(span); value.doTruncateOrDeleteAndEmitLifeChange(span);
if (value.isCanonical()) { if (value.isCanonical()) {
@ -264,7 +269,6 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject {
@Override @Override
public void removeTree(Range<Long> span) { public void removeTree(Range<Long> span) {
try (LockHold hold = manager.trace.lockWrite()) { try (LockHold hold = manager.trace.lockWrite()) {
getCanonicalParents(span).forEach(v -> v.doTruncateOrDeleteAndEmitLifeChange(span));
doRemoveTree(span); doRemoveTree(span);
} }
} }
@ -327,10 +331,14 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject {
return ifCls.cast(ifaces.get(ifCls)); return ifCls.cast(ifaces.get(ifCls));
} }
protected Collection<? extends DBTraceObjectValue> doGetParents() {
return manager.valuesByChild.get(this);
}
@Override @Override
public Collection<? extends DBTraceObjectValue> getParents() { public Collection<? extends DBTraceObjectValue> getParents() {
try (LockHold hold = manager.trace.lockRead()) { 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 @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) { Range<Long> span, PathPredicates rootPredicates) {
try (LockHold hold = manager.trace.lockRead()) { try (LockHold hold = manager.trace.lockRead()) {
return doStreamVisitor(span, new InternalAncestorsVisitor(rootPredicates)); return doStreamVisitor(span, new InternalAncestorsRootVisitor(rootPredicates));
} }
} }
@Override @Override
public Stream<? extends DBTraceObjectValPath> getSuccessors( public Stream<? extends DBTraceObjectValPath> getSuccessors(
Range<Long> span, PathPredicates relativePredicates) { Range<Long> span, PathPredicates relativePredicates) {
DBTraceObjectValPath empty = DBTraceObjectValPath.of();
try (LockHold hold = manager.trace.lockRead()) { try (LockHold hold = manager.trace.lockRead()) {
Stream<? extends DBTraceObjectValPath> succcessors = Stream<? extends DBTraceObjectValPath> succcessors =
doStreamVisitor(span, new InternalSuccessorsVisitor(relativePredicates)); doStreamVisitor(span, new InternalSuccessorsRelativeVisitor(relativePredicates));
if (relativePredicates.matches(List.of())) { if (relativePredicates.matches(List.of())) {
// Pre-cat the empty path (not the empty stream) // 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; return succcessors;
} }
@ -794,7 +814,7 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject {
Class<? extends TargetObject> targetIf) { Class<? extends TargetObject> targetIf) {
// This is a sort of meet-in-the-middle. The type search must originate from the root // This is a sort of meet-in-the-middle. The type search must originate from the root
PathMatcher matcher = getManager().getRootSchema().searchFor(targetIf, false); PathMatcher matcher = getManager().getRootSchema().searchFor(targetIf, false);
return getAncestors(span, matcher); return getAncestorsRoot(span, matcher);
} }
@Override @Override

View file

@ -23,6 +23,7 @@ import ghidra.trace.database.map.DBTraceAddressSnapRangePropertyMapTree;
import ghidra.trace.database.map.DBTraceAddressSnapRangePropertyMapTree.AbstractDBTraceAddressSnapRangePropertyMapData; import ghidra.trace.database.map.DBTraceAddressSnapRangePropertyMapTree.AbstractDBTraceAddressSnapRangePropertyMapData;
import ghidra.trace.database.target.DBTraceObjectValue.DBTraceObjectDBFieldCodec; import ghidra.trace.database.target.DBTraceObjectValue.DBTraceObjectDBFieldCodec;
import ghidra.trace.model.Trace; import ghidra.trace.model.Trace;
import ghidra.trace.model.target.TraceObjectKeyPath;
import ghidra.trace.model.target.TraceObjectValue; import ghidra.trace.model.target.TraceObjectValue;
import ghidra.trace.util.TraceAddressSpace; import ghidra.trace.util.TraceAddressSpace;
import ghidra.util.LockHold; import ghidra.util.LockHold;
@ -66,6 +67,12 @@ public class DBTraceObjectAddressRangeValue
this.manager = manager; this.manager = manager;
} }
@Override
public String toString() {
return getClass().getSimpleName() + ": parent=" + parent + ", key=" + entryKey +
", lifespan=" + getLifespan() + ", value=" + getValue();
}
@Override @Override
protected void setRecordValue(DBTraceObjectAddressRangeValue value) { protected void setRecordValue(DBTraceObjectAddressRangeValue value) {
// Nothing to do. I am the value // Nothing to do. I am the value
@ -121,11 +128,23 @@ public class DBTraceObjectAddressRangeValue
throw new ClassCastException(); throw new ClassCastException();
} }
@Override
public boolean isObject() {
return false;
}
@Override @Override
public DBTraceObject getChildOrNull() { public DBTraceObject getChildOrNull() {
return null; return null;
} }
@Override
public TraceObjectKeyPath getCanonicalPath() {
try (LockHold hold = manager.trace.lockRead()) {
return parent.getCanonicalPath().extend(entryKey);
}
}
@Override @Override
public boolean isCanonical() { public boolean isCanonical() {
return false; return false;

View file

@ -368,7 +368,7 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager
if (rootVal == null) { if (rootVal == null) {
return Stream.of(); 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.database.target.InternalTreeTraversal.Visitor;
import ghidra.trace.model.Trace; import ghidra.trace.model.Trace;
import ghidra.trace.model.target.TraceObject; import ghidra.trace.model.target.TraceObject;
import ghidra.trace.model.target.TraceObjectKeyPath;
import ghidra.util.LockHold; import ghidra.util.LockHold;
import ghidra.util.database.*; import ghidra.util.database.*;
import ghidra.util.database.DBCachedObjectStoreFactory.AbstractDBFieldCodec; import ghidra.util.database.DBCachedObjectStoreFactory.AbstractDBFieldCodec;
@ -275,6 +276,11 @@ public class DBTraceObjectValue extends DBAnnotatedObject implements InternalTra
return (DBTraceObject) getValue(); return (DBTraceObject) getValue();
} }
@Override
public boolean isObject() {
return child != null;
}
@Override @Override
public DBTraceObject getChildOrNull() { public DBTraceObject getChildOrNull() {
return child; return child;
@ -320,6 +326,10 @@ public class DBTraceObjectValue extends DBAnnotatedObject implements InternalTra
return InternalTreeTraversal.INSTANCE.walkValue(visitor, this, span, null); return InternalTreeTraversal.INSTANCE.walkValue(visitor, this, span, null);
} }
protected TraceObjectKeyPath doGetCanonicalPath() {
return triple.parent.getCanonicalPath().extend(triple.key);
}
protected boolean doIsCanonical() { protected boolean doIsCanonical() {
if (child == null) { if (child == null) {
return false; return false;
@ -327,7 +337,14 @@ public class DBTraceObjectValue extends DBAnnotatedObject implements InternalTra
if (triple.parent == null) { if (triple.parent == null) {
return true; 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 @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.SpanIntersectingVisitor;
import ghidra.trace.database.target.InternalTreeTraversal.VisitResult; import ghidra.trace.database.target.InternalTreeTraversal.VisitResult;
public class InternalAncestorsVisitor implements SpanIntersectingVisitor { public class InternalAncestorsRootVisitor implements SpanIntersectingVisitor {
protected final PathPredicates predicates; protected final PathPredicates predicates;
public InternalAncestorsVisitor(PathPredicates predicates) { public InternalAncestorsRootVisitor(PathPredicates predicates) {
this.predicates = 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.SpanIntersectingVisitor;
import ghidra.trace.database.target.InternalTreeTraversal.VisitResult; import ghidra.trace.database.target.InternalTreeTraversal.VisitResult;
public class InternalSuccessorsVisitor implements SpanIntersectingVisitor { public class InternalSuccessorsRelativeVisitor implements SpanIntersectingVisitor {
protected final PathPredicates predicates; protected final PathPredicates predicates;
public InternalSuccessorsVisitor(PathPredicates predicates) { public InternalSuccessorsRelativeVisitor(PathPredicates predicates) {
this.predicates = predicates; this.predicates = predicates;
} }

View file

@ -125,6 +125,14 @@ public class TraceDomainObjectListener implements DomainObjectListener {
typedMap.put(type, handler); 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, protected <T, U> void listenFor(TraceChangeType<T, U> type,
AffectedAndValuesOnlyHandler<? super T, ? super U> handler) { AffectedAndValuesOnlyHandler<? super T, ? super U> handler) {
typedMap.put(type, 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.Range;
import com.google.common.collect.RangeSet; import com.google.common.collect.RangeSet;
import ghidra.dbg.target.TargetMethod;
import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.dbg.util.PathPredicates; import ghidra.dbg.util.PathPredicates;
@ -38,6 +39,8 @@ import ghidra.trace.model.TraceUniqueObject;
* In many cases, such interfaces are just wrappers. * In many cases, such interfaces are just wrappers.
*/ */
public interface TraceObject extends TraceUniqueObject { public interface TraceObject extends TraceUniqueObject {
String EXTRA_INTERFACES_ATTRIBUTE_NAME = "_extra_ifs";
/** /**
* Get the trace containing this object * 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 the minimum lifespan of edges from the root to this object
* @param resolution the rule for handling duplicate keys when setting values. * @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 * 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 * Remove this object and its successors from their canonical paths for the given span
* *
* <p> * <p>
* Truncate the lifespans of this object's canonical parent value and all canonical values * Truncate the lifespans of this object's parent values and all canonical values succeeding
* succeeding this object. If a truncated value's lifespan is contained in the given span, the * this object. If a truncated value's lifespan is contained in the given span, the value will
* value will be deleted. * be deleted.
* *
* @param span the span during which this object and its canonical successors should be removed * @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 * @param rootPredicates the predicates for matching path keys, relative to the root
* @return the stream of matching paths to values * @return the stream of matching paths to values
*/ */
Stream<? extends TraceObjectValPath> getAncestors(Range<Long> span, Stream<? extends TraceObjectValPath> getAncestorsRoot(Range<Long> span,
PathPredicates rootPredicates); 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 * Stream all successor values of this object matching the given predicates, intersecting the
* given span * given span
@ -466,4 +481,30 @@ public interface TraceObject extends TraceUniqueObject {
*/ */
@Override @Override
boolean isDeleted(); 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)) { if (!predicates.ancestorMatches(keyList, false)) {
return Stream.of(); return Stream.of();
} }
Stream<TraceObjectKeyPath> ancestry =
isRoot() ? Stream.of() : parent().streamMatchingAncestry(predicates);
if (predicates.matches(keyList)) { 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 com.google.common.collect.Range;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.trace.model.Trace; import ghidra.trace.model.Trace;
import ghidra.trace.model.target.TraceObject.ConflictResolution; import ghidra.trace.model.target.TraceObject.ConflictResolution;
@ -43,6 +44,17 @@ public interface TraceObjectValue {
*/ */
String getEntryKey(); 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 * Get the value
* *
@ -58,6 +70,13 @@ public interface TraceObjectValue {
*/ */
TraceObject getChild(); 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 * Check if this value represents its child's canonical location
* *
@ -69,6 +88,15 @@ public interface TraceObjectValue {
*/ */
boolean isCanonical(); 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 * Set the lifespan of this entry, truncating duplicates
* *
@ -157,4 +185,17 @@ public interface TraceObjectValue {
* if a second is created. * if a second is created.
*/ */
TraceObjectValue truncateOrDelete(Range<Long> span); 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; import com.google.common.collect.Range;
public class RangeCursorTableHeaderRenderer<N extends Number & Comparable<N>> 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 int ARROW_SIZE = 10;
protected final static Polygon ARROW = new Polygon( protected final static Polygon ARROW = new Polygon(
new int[] { 0, -ARROW_SIZE, -ARROW_SIZE }, new int[] { 0, -ARROW_SIZE, -ARROW_SIZE },
new int[] { 0, ARROW_SIZE, -ARROW_SIZE }, 3); 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 double span = 1;
protected Range<N> fullRange;
protected N pos; protected N pos;
protected double doublePos; protected double doublePos;
@Override
public void setFullRange(Range<N> fullRange) { public void setFullRange(Range<N> fullRange) {
this.fullRange = RangeTableCellRenderer.validateViewRange(fullRange); this.fullRangeDouble = RangedRenderer.validateViewRange(fullRange);
this.span = this.fullRange.upperEndpoint() - this.fullRange.lowerEndpoint(); this.span = this.fullRangeDouble.upperEndpoint() - this.fullRangeDouble.lowerEndpoint();
} }
public void setCursorPosition(N pos) { public void setCursorPosition(N pos) {
@ -51,6 +54,7 @@ public class RangeCursorTableHeaderRenderer<N extends Number & Comparable<N>>
@Override @Override
protected void paintChildren(Graphics g) { protected void paintChildren(Graphics g) {
super.paintChildren(g); super.paintChildren(g);
// The cursor should occlude the children
paintCursor(g); paintCursor(g);
} }
@ -58,7 +62,7 @@ public class RangeCursorTableHeaderRenderer<N extends Number & Comparable<N>>
Graphics2D g = (Graphics2D) parentG.create(); Graphics2D g = (Graphics2D) parentG.create();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 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.translate(x, getHeight());
g.rotate(Math.PI / 2); g.rotate(Math.PI / 2);
g.setColor(getForeground()); g.setColor(getForeground());
@ -117,7 +121,8 @@ public class RangeCursorTableHeaderRenderer<N extends Number & Comparable<N>>
} }
TableColumn col = colModel.getColumn(viewColIdx); 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); listener.accept(pos);
} }
}; };
@ -128,4 +133,19 @@ public class RangeCursorTableHeaderRenderer<N extends Number & Comparable<N>>
public N getCursorPosition() { public N getCursorPosition() {
return pos; 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; import ghidra.util.table.column.AbstractGColumnRenderer;
public class RangeTableCellRenderer<N extends Number & Comparable<N>> 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 double span = 1;
protected Range<N> fullRange; protected Range<N> fullRange;
protected Range<N> dataRange; protected Range<N> dataRange;
public static Range<Double> validateViewRange(Range<? extends Number> fullRange) { @Override
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());
}
public void setFullRange(Range<N> fullRange) { public void setFullRange(Range<N> fullRange) {
this.fullRange = fullRange; this.fullRange = fullRange;
this.doubleFullRange = validateViewRange(fullRange); this.fullRangeDouble = RangedRenderer.validateViewRange(fullRange);
this.span = this.doubleFullRange.upperEndpoint() - this.doubleFullRange.lowerEndpoint(); this.span = this.fullRangeDouble.upperEndpoint() - this.fullRangeDouble.lowerEndpoint();
} }
@Override @Override
@ -67,38 +59,24 @@ public class RangeTableCellRenderer<N extends Number & Comparable<N>>
if (dataRange == null) { if (dataRange == null) {
return; 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(); Graphics g = parentG.create();
g.setColor(getForeground()); g.setColor(getForeground());
paintRange(g, dataRange);
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);
} }
@Override
public Range<N> getFullRange() { public Range<N> getFullRange() {
return fullRange; 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; import utilities.util.reflection.ReflectionUtilities;
/** /**
* An Table Column is an interface that should be implemented by each class that provides * An Table Column is an interface that should be implemented by each class that provides a field
* a field (column) of an object based table (each row relates to a particular type of object). * (column) of an object based table (each row relates to a particular type of object). It
* It determines the appropriate cell object for use by the table column this field represents. * determines the appropriate cell object for use by the table column this field represents. It can
* It can then return the appropriate object to display in the table cell for the indicated * then return the appropriate object to display in the table cell for the indicated row object.
* row object.
* *
* Implementations of this interface must provide a public default constructor. * Implementations of this interface must provide a public default constructor.
* *
* @param <ROW_TYPE> The row object class supported by this column * @param <ROW_TYPE> The row object class supported by this column
* @param <COLUMN_TYPE> The column 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 * @param <DATA_SOURCE> The object class type that will be passed to see
* see <code>getValue(ROW_TYPE, Settings, DATA_SOURCE, ServiceProvider)</code> * <code>getValue(ROW_TYPE, Settings, DATA_SOURCE, ServiceProvider)</code>
*/ */
public abstract class AbstractDynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE> public abstract class AbstractDynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE>
implements DynamicTableColumn<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; return -1;
} }
@Override
public Comparator<COLUMN_TYPE> getComparator() { public Comparator<COLUMN_TYPE> getComparator() {
return null; return null;
} }
@Override
public Comparator<COLUMN_TYPE> getComparator(DynamicColumnTableModel<?> model,
int columnIndex) {
return getComparator();
}
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
// enforced by the compiler // enforced by the compiler

View file

@ -24,48 +24,52 @@ import ghidra.framework.plugintool.ServiceProvider;
import ghidra.util.table.column.GColumnRenderer; import ghidra.util.table.column.GColumnRenderer;
/** /**
* The root interface for defining columns for {@link GDynamicColumnTableModel}s. The * The root interface for defining columns for {@link DynamicColumnTableModel}s. The class allows
* class allows you to create objects for tables that know how to give a column value for a * you to create objects for tables that know how to give a column value for a given row.
* given row.
* *
* @param <ROW_TYPE> The row object class supported by this column * @param <ROW_TYPE> The row object class supported by this column
* @param <COLUMN_TYPE> The column 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 * @param <DATA_SOURCE> The object class type that will be passed to see
* see <code>getValue(ROW_TYPE, Settings, DATA_SOURCE, ServiceProvider)</code> * <code>getValue(ROW_TYPE, Settings, DATA_SOURCE, ServiceProvider)</code>
*/ */
public interface DynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE> { public interface DynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE> {
/** /**
* Determines the unique column heading that may be used to identify a column instance. * Determines the unique column heading that may be used to identify a column instance. This
* This name must be non-changing and is used to save/restore state information. * name must be non-changing and is used to save/restore state information.
*
* @return the field instance name. * @return the field instance name.
*/ */
public String getColumnName(); public String getColumnName();
/** /**
* Determines the class of object that is associated with this field (column). * Determines the class of object that is associated with this field (column).
*
* @return the column class * @return the column class
*/ */
public Class<COLUMN_TYPE> getColumnClass(); 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. * preferred size or -1.
*
* @return the preferred width for this column. * @return the preferred width for this column.
*/ */
public int getColumnPreferredWidth(); public int getColumnPreferredWidth();
/** /**
* Returns the single class type of the data that this table field can use to * Returns the single class type of the data that this table field can use to generate columnar
* generate columnar data. * data.
* @return 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(); public Class<ROW_TYPE> getSupportedRowType();
/** /**
* Creates an object that is appropriate for this field (table column) and for the * Creates an object that is appropriate for this field (table column) and for the object that
* object that is associated with this row of the table. * is associated with this row of the table.
*
* @param rowObject the object associated with the row in the table. * @param rowObject the object associated with the row in the table.
* @param settings field settings * @param settings field settings
* @param data the expected data object, as defined by the DATA_SOURCE type * @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. * 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 * <P>
* ensures that the text used for filtering matches what the users sees (via the * This method allows columns to define custom rendering. The interface returned here ensures
* that the text used for filtering matches what the users sees (via the
* {@link GColumnRenderer#getFilterString(Object, Settings)} method). * {@link GColumnRenderer#getFilterString(Object, Settings)} method).
* *
* <P>Note: some types should not make use of the aforementioned filter string. These types * <P>
* include the {@link Number} wrapper types, {@link Date} and {@link Enum}s. (This is * Note: some types should not make use of the aforementioned filter string. These types include
* because the filtering system works naturally with these types.) See {@link GColumnRenderer}. * 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 * @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. * Returns a list of settings definitions for this field.
*
* @return list of settings definitions for this field. * @return list of settings definitions for this field.
*/ */
public SettingsDefinition[] getSettingsDefinitions(); public SettingsDefinition[] getSettingsDefinitions();
/** /**
* Gets the maximum number of text display lines needed for any given cell with the * Gets the maximum number of text display lines needed for any given cell with the specified
* specified settings. * settings.
*
* @param settings field settings * @param settings field settings
* @return maximum number of lines needed * @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. * Determines the column heading that will be displayed.
*
* @param settings the settings * @param settings the settings
* @return the field name to display as the column heading. * @return the field name to display as the column heading.
*/ */
public String getColumnDisplayName(Settings settings); public String getColumnDisplayName(Settings settings);
/** /**
* Returns 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. *
* @return a description of this column. This may be used as a tooltip for the column header.
*/ */
public String getColumnDescription(); public String getColumnDescription();
/** /**
* Returns a value that is unique for this table column. This is different than getting * Returns a value that is unique for this table column. This is different than getting the
* the display name, which may be shared by different columns. * display name, which may be shared by different columns.
*
* @return the identifier * @return the identifier
*/ */
public String getUniqueIdentifier(); 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 * Implementors should return <code>null</code> if they do not wish to provider a comparator
*
* @return the 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; import utilities.util.reflection.ReflectionUtilities;
/** /**
* An abstract table model for showing DynamicTableColumns where each row is based on an * An abstract table model for showing DynamicTableColumns where each row is based on an object of
* object of type ROW_TYPE. The client is responsible for implementing * type ROW_TYPE. The client is responsible for implementing {@link #createTableColumnDescriptor()}.
* {@link #createTableColumnDescriptor()}. This method specifies which default columns the * This method specifies which default columns the table should have and whether they should be
* table should have and whether they should be visible or hidden. Hidden columns can be * visible or hidden. Hidden columns can be made visible through the UI.
* made visible through the UI.
* <p> * <p>
* This model will also discover other system columns that understand how to render * 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, * your row type, then this model will load columns for each type for which a mapper was created,
* all as optional, hidden columns. * all as optional, hidden columns.
* <p> * <p>
* The various attributes of the columns of this model (visibility, position, size, etc) are * The various attributes of the columns of this model (visibility, position, size, etc) are saved
* saved to disk as tool preferences when the user exits the tool. * to disk as tool preferences when the user exits the tool.
* <p> * <p>
* Implementation Note: this model loads all columns, specific and discovered, as being visible. * Implementation Note: this model loads all columns, specific and discovered, as being visible.
* Then, during initialization, the {@link TableColumnModelState} class will * Then, during initialization, the {@link TableColumnModelState} class will either hide all
* either hide all non-default columns, or reload the column state if any * non-default columns, or reload the column state if any previous saved state is found.
* previous saved state is found.
* *
* @param <ROW_TYPE> the row object class for this table model. * @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 * @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 * object will be given to the {@link DynamicTableColumn} objects used by this table
* table model when * model when
* {@link DynamicTableColumn#getValue(Object, ghidra.docking.settings.Settings, Object, ServiceProvider)} * {@link DynamicTableColumn#getValue(Object, ghidra.docking.settings.Settings, Object, ServiceProvider)}
* is called. * is called.
*/ */
public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE> public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
extends AbstractSortedTableModel<ROW_TYPE> 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 * Allows clients to defer column creation until after this parent class's constructor has been
* been called. This method will not restore any column settings that have been changed * called. This method will not restore any column settings that have been changed after
* after construction. Thus, this method is intended only to be called during the * construction. Thus, this method is intended only to be called during the construction
* construction process. * process.
*/ */
protected void reloadColumns() { 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 * This differs from {@link #createSortComparator(int)} in that the other method creates a
* creates a comparator that operates on a full row value, whereas this method operates on * comparator that operates on a full row value, whereas this method operates on column values.
* column values.
* *
* @param columnIndex the column index * @param columnIndex the column index
* @return a comparator for the specific column values * @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 @SuppressWarnings("unchecked") // the column provides the values itself; safe cast
protected Comparator<Object> createSortComparatorForColumn(int columnIndex) { protected Comparator<Object> createSortComparatorForColumn(int columnIndex) {
DynamicTableColumn<ROW_TYPE, ?, ?> column = getColumn(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; 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 * implementations to add custom column objects, rather than relying on generic, discovered
* DynamicTableColumn implementations. * 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 * @param column The field to add
*/ */
protected void addTableColumn(DynamicTableColumn<ROW_TYPE, ?, ?> column) { 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 * implementations to add custom column objects, rather than relying on generic, discovered
* DynamicTableColumn implementations. * 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 * @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. * Adds the given field at the given index to the list of fields in this class. This method is
* This method is intended for implementations to add custom column objects, rather than * intended for implementations to add custom column objects, rather than relying on generic,
* relying on generic, discovered DynamicTableColumn implementations. * discovered DynamicTableColumn implementations.
* <p> * <p>
* <b>Note: this method assumes that the columns have already been sorted.</b> * <b>Note: this method assumes that the columns have already been sorted.</b>
*
* @param column The field to add. * @param column The field to add.
* @param index The index at which to add the field. If the index value is invalid (negative * @param index The index at which to add the field. If the index value is invalid (negative or
* or greater than the number of columns), then the column will be added to the * greater than the number of columns), then the column will be added to the end of
* end of the columns list. * the columns list.
* @param isDefault true if this is a default column * @param isDefault true if this is a default column
*/ */
protected void addTableColumn(DynamicTableColumn<ROW_TYPE, ?, ?> column, int index, 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 * Removes the given columns from this model. This method allows the client to remove multiple
* multiple columns at once, firing only one event when the work is finished. * columns at once, firing only one event when the work is finished.
* *
* @param columns the columns to remove * @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 * 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). * that it was specified by the model and not discovered).
*
* @param modelIndex the index of the column in the model. * @param modelIndex the index of the column in the model.
* @return true if the column is a default. * @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. * 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(); 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. * Gets the special table cell renderer for the specified table field column. A null value
* A null value indicates that this field uses a default cell renderer. * indicates that this field uses a default cell renderer.
* *
* @param index the model column index * @param index the model column index
* @return a table cell renderer for this field. Otherwise, null if a default * @return a table cell renderer for this field. Otherwise, null if a default renderer should be
* renderer should be used. * used.
*/ */
@Override @Override
public TableCellRenderer getRenderer(int index) { 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 * Gets the maximum number of text display lines needed for any given cell within the specified
* specified column. * column.
*
* @param index column field index * @param index column field index
* @return maximum number of lines needed for specified column * @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; import ghidra.util.table.column.GColumnRenderer;
/** /**
* A class that is an Adapter in order to allow for the use of existing * A class that is an Adapter in order to allow for the use of existing {@link DynamicTableColumn}s
* {@link DynamicTableColumn}s when the actual row type of the table is * when the actual row type of the table is not the same as the row type that the
* not the same as the row type that the {@link DynamicTableColumn} supports. * {@link DynamicTableColumn} supports.
* *
* @param <ROW_TYPE> The table's actual row type * @param <ROW_TYPE> The table's actual row type
* @param <EXPECTED_ROW_TYPE> The row type expected by the given {@link DynamicTableColumn} * @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 <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 * @param <DATA_SOURCE> the type of the data for each column; can be Object for columns that do not
* do not have a data source * have a data source
*/ */
public class MappedTableColumn<ROW_TYPE, EXPECTED_ROW_TYPE, COLUMN_TYPE, DATA_SOURCE> public class MappedTableColumn<ROW_TYPE, EXPECTED_ROW_TYPE, COLUMN_TYPE, DATA_SOURCE>
extends AbstractDynamicTableColumn<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 @Override
public Comparator<COLUMN_TYPE> getComparator() { public Comparator<COLUMN_TYPE> getComparator(DynamicColumnTableModel<?> model,
return tableColumn.getComparator(); int columnIndex) {
return tableColumn.getComparator(model, columnIndex);
} }
@Override @Override