GP-890: Added dialog for adding mappings and fixing various db/ui issues re/ length.

This commit is contained in:
Dan
2021-04-28 15:43:27 -04:00
parent bd2ac52c9b
commit e6fd14f88c
13 changed files with 967 additions and 111 deletions

View File

@@ -0,0 +1,378 @@
/* ###
* 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.modules;
import java.awt.Component;
import java.awt.Font;
import java.awt.event.*;
import java.math.BigInteger;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import com.google.common.collect.Range;
import docking.DialogComponentProvider;
import docking.widgets.model.GAddressRangeField;
import docking.widgets.model.GLifespanField;
import ghidra.app.services.DebuggerStaticMappingService;
import ghidra.program.model.address.*;
import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramLocation;
import ghidra.trace.model.*;
import ghidra.trace.model.modules.TraceConflictedMappingException;
import ghidra.util.MathUtilities;
import ghidra.util.database.UndoableTransaction;
import ghidra.util.layout.PairLayout;
public class DebuggerAddMappingDialog extends DialogComponentProvider {
private static final String HEX_BIT64 = "0x" + BigInteger.ONE.shiftLeft(64).toString(16);
private DebuggerStaticMappingService mappingService;
private Program program;
private Trace trace;
private final JLabel labelProg = new JLabel();
private final GAddressRangeField fieldProgRange = new GAddressRangeField();
private final JLabel labelTrace = new JLabel();
private final GAddressRangeField fieldTraceRange = new GAddressRangeField();
private final JTextField fieldLength = new JTextField();
private final GLifespanField fieldSpan = new GLifespanField();
public DebuggerAddMappingDialog() {
super("Add Static Mapping", false, false, true, false);
populateComponents();
}
protected static void rigFocusAndEnter(Component c, Runnable runnable) {
c.addFocusListener(new FocusAdapter() {
@Override
public void focusLost(FocusEvent e) {
runnable.run();
}
});
c.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
runnable.run();
}
}
});
}
protected void populateComponents() {
JPanel panel = new JPanel(new PairLayout(5, 5));
panel.setBorder(new EmptyBorder(10, 10, 10, 10));
panel.add(new JLabel("Program: "));
panel.add(labelProg);
panel.add(new JLabel("Static Range: "));
panel.add(fieldProgRange);
panel.add(new JLabel("Trace: "));
panel.add(labelTrace);
panel.add(new JLabel("Dynamic Range: "));
panel.add(fieldTraceRange);
panel.add(new JLabel("Length: "));
fieldLength.setFont(Font.decode("monospaced"));
panel.add(fieldLength);
panel.add(new JLabel("Lifespan: "));
panel.add(fieldSpan);
rigFocusAndEnter(fieldProgRange, this::progRangeChanged);
rigFocusAndEnter(fieldTraceRange, this::traceRangeChanged);
rigFocusAndEnter(fieldLength, this::lengthChanged);
rigFocusAndEnter(fieldSpan, this::spanChanged);
addWorkPanel(panel);
addApplyButton();
addDismissButton();
setDefaultButton(null);
}
public void setMappingService(DebuggerStaticMappingService mappingService) {
this.mappingService = mappingService;
}
protected static void revalidateByLength(GAddressRangeField adjusted,
GAddressRangeField other) {
AddressRange adjRange = adjusted.getRange();
if (adjRange == null) {
return;
}
long lengthMinus1 = adjRange.getMaxAddress().subtract(adjRange.getMinAddress());
AddressRange otherRange = other.getRange();
if (otherRange == null) {
return;
}
long maxLengthMinus1 =
otherRange.getAddressSpace().getMaxAddress().subtract(otherRange.getMinAddress());
if (Long.compareUnsigned(lengthMinus1, maxLengthMinus1) > 0) {
adjusted.setRange(range(adjRange.getMinAddress(), maxLengthMinus1));
}
}
protected void revalidateProgRange() {
/**
* Not sure there's much to do here wrt/ the program. Might be nice to warn (but not
* prohibit) when range is not in memory. NB: A range spanning all blocks will likely
* contain some dead space.
*/
revalidateByLength(fieldProgRange, fieldTraceRange);
}
protected void revalidateTraceRange() {
// Similarly nothing to really do wrt/ the trace.
revalidateByLength(fieldTraceRange, fieldProgRange);
}
protected static long lengthMin(long a, long b) {
if (a == 0) {
return b;
}
if (b == 0) {
return a;
}
return MathUtilities.unsignedMin(a, b);
}
protected static long revalidateLengthByRange(AddressRange range, long length) {
long maxLength =
range.getAddressSpace().getMaxAddress().subtract(range.getMinAddress()) + 1;
return lengthMin(length, maxLength);
}
protected void setFieldLength(long length) {
if (length == 0) {
fieldLength.setText(HEX_BIT64);
}
else {
fieldLength.setText("0x" + Long.toHexString(length));
}
}
public long getLength() {
return parseLength(fieldLength.getText(), 1);
}
protected void revalidateLength() {
long length;
if (fieldLength.getText().trim().startsWith("-")) {
length = 1;
}
else {
length = getLength();
}
length = revalidateLengthByRange(fieldProgRange.getRange(), length);
length = revalidateLengthByRange(fieldTraceRange.getRange(), length);
setFieldLength(length);
}
protected void revalidateSpan() {
// Yeah, nothing to do
}
protected static AddressRange range(Address min, long lengthMinus1) {
return new AddressRangeImpl(min, min.addWrap(lengthMinus1));
}
protected void adjustLengthToProgRange() {
long length = fieldProgRange.getRange().getLength();
setFieldLength(length);
}
protected void adjustLengthToTraceRange() {
long length = fieldTraceRange.getRange().getLength();
setFieldLength(length);
}
protected void adjustRangeToLength(GAddressRangeField field) {
AddressRange range = field.getRange();
if (range == null) {
return;
}
Address min = range.getMinAddress();
field.setRange(range(min, getLength() - 1));
}
protected void adjustProgRangeToLength() {
adjustRangeToLength(fieldProgRange);
}
protected void adjustTraceRangeToLength() {
adjustRangeToLength(fieldTraceRange);
}
protected void progRangeChanged() {
revalidateProgRange();
adjustLengthToProgRange();
adjustTraceRangeToLength();
}
protected void traceRangeChanged() {
revalidateTraceRange();
adjustLengthToTraceRange();
adjustProgRangeToLength();
}
protected void lengthChanged() {
revalidateLength();
adjustProgRangeToLength();
adjustTraceRangeToLength();
}
protected void spanChanged() {
revalidateSpan();
}
/**
* Parses a value from 1 to 1<<64. Any value outside the range is "clipped" into the range.
*
* <p>
* Note that a returned value of 0 indicates 2 to the power 64, which is just 1 too high to fit
* into a 64-bit long.
*
* @param text the text to parse
* @param defaultVal the default value should parsing fail altogether
* @return the length, where 0 indicates {@code 1 << 64}.
*/
protected static long parseLength(String text, long defaultVal) {
text = text.trim();
String post;
int radix;
if (text.startsWith("-")) {
return 0;
}
if (text.startsWith("0x")) {
post = text.substring(2);
radix = 16;
}
else {
post = text;
radix = 10;
}
BigInteger bi;
try {
bi = new BigInteger(post, radix);
}
catch (NumberFormatException e) {
return defaultVal;
}
if (bi.equals(BigInteger.ZERO)) {
return 1;
}
if (bi.bitLength() > 64) {
return 0; // indicates 2**64, the max length
}
return bi.longValue(); // Do not use exact. It checks bitLength again, and considers sign.
}
@Override
protected void applyCallback() {
TraceLocation from = new DefaultTraceLocation(trace, null, fieldSpan.getLifespan(),
fieldTraceRange.getRange().getMinAddress());
ProgramLocation to = new ProgramLocation(program,
fieldProgRange.getRange().getMinAddress());
try (UndoableTransaction tid =
UndoableTransaction.start(trace, "Add Static Mapping", false)) {
mappingService.addMapping(from, to, getLength(), true);
tid.commit();
}
catch (TraceConflictedMappingException e) {
throw new AssertionError(e); // I said truncateExisting
}
}
/**
* Set the values of the fields
*
* @param program the program
* @param trace the trace
* @param progStart the starting static address
* @param traceStart the starting dynamic address
* @param length the length (0 indicates the entire 64-bit range)
* @param lifespan the lifespan
* @throws AddressOverflowException if the length is too large for either space
*/
public void setValues(Program program, Trace trace, Address progStart,
Address traceStart, long length, Range<Long> lifespan) throws AddressOverflowException {
// NB. This dialog will not validate these. The caller is responsible.
this.program = program;
this.trace = trace;
this.fieldProgRange.setAddressFactory(program.getAddressFactory());
this.fieldProgRange.setRange(range(progStart, length - 1));
this.fieldTraceRange.setAddressFactory(trace.getBaseAddressFactory());
this.fieldTraceRange.setRange(range(traceStart, length - 1));
setFieldLength(length);
this.fieldSpan.setLifespan(lifespan);
}
protected void setEnabled(boolean enabled) {
applyButton.setEnabled(enabled);
fieldProgRange.setEnabled(enabled);
fieldTraceRange.setEnabled(enabled);
fieldLength.setEnabled(enabled);
fieldSpan.setEnabled(enabled);
}
public void setTrace(Trace trace) {
this.trace = trace;
if (trace != null) {
labelTrace.setText(trace.getName());
fieldTraceRange.setAddressFactory(trace.getBaseAddressFactory());
}
else {
labelTrace.setText("[No Trace]");
fieldTraceRange.setAddressFactory(null);
}
if (program != null && trace != null) {
traceRangeChanged();
setEnabled(true);
}
else {
setEnabled(false);
}
}
public void setProgram(Program program) {
this.program = program;
if (program != null) {
labelProg.setText(program.getName());
fieldProgRange.setAddressFactory(program.getAddressFactory());
}
else {
labelProg.setText("[No Program]");
fieldProgRange.setAddressFactory(null);
}
if (program != null && trace != null) {
progRangeChanged();
setEnabled(true);
}
else {
setEnabled(false);
}
}
}

View File

@@ -15,35 +15,30 @@
*/
package ghidra.app.plugin.core.debug.gui.modules;
import ghidra.app.events.ProgramLocationPluginEvent;
import ghidra.app.events.ProgramSelectionPluginEvent;
import ghidra.app.events.ProgramActivatedPluginEvent;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.debug.AbstractDebuggerPlugin;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.event.*;
import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent;
import ghidra.app.services.DebuggerStaticMappingService;
import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
@PluginInfo( //
shortDescription = "Debugger static mapping manager", //
description = "GUI to manage static mappings", //
category = PluginCategoryNames.DEBUGGER, //
packageName = DebuggerPluginPackage.NAME, //
status = PluginStatus.RELEASED, //
eventsConsumed = {
TraceActivatedPluginEvent.class, //
ProgramLocationPluginEvent.class, //
ProgramSelectionPluginEvent.class, //
TraceLocationPluginEvent.class, //
TraceSelectionPluginEvent.class //
}, //
servicesRequired = { //
DebuggerStaticMappingService.class, //
DebuggerTraceManagerService.class, //
} //
)
@PluginInfo(
shortDescription = "Debugger static mapping manager",
description = "GUI to manage static mappings",
category = PluginCategoryNames.DEBUGGER,
packageName = DebuggerPluginPackage.NAME,
status = PluginStatus.RELEASED,
eventsConsumed = {
TraceActivatedPluginEvent.class,
ProgramActivatedPluginEvent.class,
},
servicesRequired = {
DebuggerStaticMappingService.class,
DebuggerTraceManagerService.class,
})
public class DebuggerStaticMappingPlugin extends AbstractDebuggerPlugin {
protected DebuggerStaticMappingProvider provider;
@@ -70,17 +65,9 @@ public class DebuggerStaticMappingPlugin extends AbstractDebuggerPlugin {
TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event;
provider.setTrace(ev.getActiveCoordinates().getTrace());
}
if (event instanceof ProgramLocationPluginEvent) {
provider.contextChanged();
}
if (event instanceof ProgramSelectionPluginEvent) {
provider.contextChanged();
}
if (event instanceof TraceLocationPluginEvent) {
provider.contextChanged();
}
if (event instanceof TraceSelectionPluginEvent) {
provider.contextChanged();
if (event instanceof ProgramActivatedPluginEvent) {
ProgramActivatedPluginEvent ev = (ProgramActivatedPluginEvent) event;
provider.setProgram(ev.getActiveProgram());
}
}
}

View File

@@ -17,6 +17,7 @@ package ghidra.app.plugin.core.debug.gui.modules;
import java.awt.BorderLayout;
import java.awt.event.MouseEvent;
import java.math.BigInteger;
import java.net.URL;
import java.util.*;
import java.util.function.Function;
@@ -41,15 +42,18 @@ import ghidra.framework.model.DomainObject;
import ghidra.framework.plugintool.AutoService;
import ghidra.framework.plugintool.ComponentProviderAdapter;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressRange;
import ghidra.program.model.address.*;
import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramLocation;
import ghidra.program.util.ProgramSelection;
import ghidra.trace.model.*;
import ghidra.trace.model.Trace;
import ghidra.trace.model.Trace.TraceStaticMappingChangeType;
import ghidra.trace.model.modules.*;
import ghidra.trace.model.TraceDomainObjectListener;
import ghidra.trace.model.modules.TraceStaticMapping;
import ghidra.trace.model.modules.TraceStaticMappingManager;
import ghidra.trace.model.program.TraceProgramView;
import ghidra.util.MathUtilities;
import ghidra.util.Msg;
import ghidra.util.database.UndoableTransaction;
import ghidra.util.table.GhidraTableFilterPanel;
@@ -60,7 +64,7 @@ public class DebuggerStaticMappingProvider extends ComponentProviderAdapter
DYNAMIC_ADDRESS("Dynamic Address", Address.class, StaticMappingRow::getTraceAddress),
STATIC_URL("Static Program", URL.class, StaticMappingRow::getStaticProgramURL),
STATIC_ADDRESS("Static Address", String.class, StaticMappingRow::getStaticAddress),
LENGTH("Length", Long.class, StaticMappingRow::getLength),
LENGTH("Length", BigInteger.class, StaticMappingRow::getBigLength),
SHIFT("Shift", Long.class, StaticMappingRow::getShift),
LIFESPAN("Lifespan", Range.class, StaticMappingRow::getLifespan);
@@ -113,6 +117,8 @@ public class DebuggerStaticMappingProvider extends ComponentProviderAdapter
private final DebuggerStaticMappingPlugin plugin;
private final DebuggerAddMappingDialog addMappingDialog;
@AutoServiceConsumed
private DebuggerStaticMappingService mappingService;
// TODO: Use events to track selections? This can only work with the main listings.
@@ -146,6 +152,7 @@ public class DebuggerStaticMappingProvider extends ComponentProviderAdapter
super(plugin.getTool(), DebuggerResources.TITLE_PROVIDER_MAPPINGS, plugin.getName(), null);
this.plugin = plugin;
this.addMappingDialog = new DebuggerAddMappingDialog();
this.autoWiring = AutoService.wireServicesConsumed(plugin, this);
setIcon(DebuggerResources.ICON_PROVIDER_MAPPINGS);
@@ -157,6 +164,11 @@ public class DebuggerStaticMappingProvider extends ComponentProviderAdapter
createActions();
}
@AutoServiceConsumed
private void setMappingService(DebuggerStaticMappingService mappingService) {
addMappingDialog.setMappingService(mappingService);
}
@Override
public void addLocalAction(DockingActionIf action) {
super.addLocalAction(action);
@@ -215,7 +227,7 @@ public class DebuggerStaticMappingProvider extends ComponentProviderAdapter
statAddrCol.setCellRenderer(CustomToStringCellRenderer.MONO_OBJECT);
TableColumn lengthCol = columnModel.getColumn(StaticMappingTableColumns.LENGTH.ordinal());
// TODO: Get user column settings working. Still, should default to Hex
lengthCol.setCellRenderer(CustomToStringCellRenderer.MONO_LONG_HEX);
lengthCol.setCellRenderer(CustomToStringCellRenderer.MONO_BIG_HEX);
TableColumn shiftCol = columnModel.getColumn(StaticMappingTableColumns.SHIFT.ordinal());
shiftCol.setCellRenderer(CustomToStringCellRenderer.MONO_LONG_HEX);
}
@@ -223,7 +235,6 @@ public class DebuggerStaticMappingProvider extends ComponentProviderAdapter
protected void createActions() {
actionAdd = AddAction.builder(plugin)
.description("Add Mapping from Listing Selections")
.enabledWhen(this::haveMappableSelections)
.onAction(this::activatedAdd)
.buildAndInstallLocal(this);
actionRemove = RemoveAction.builder(plugin)
@@ -240,38 +251,10 @@ public class DebuggerStaticMappingProvider extends ComponentProviderAdapter
contextChanged();
}
private boolean haveMappableSelections(ActionContext ignore) {
// TODO: Use events to track selections/locations
if (codeViewerService == null || listingService == null) {
return false;
}
ProgramLocation progLoc = codeViewerService.getCurrentLocation();
ProgramLocation traceLoc = listingService.getCurrentLocation();
if (progLoc == null || traceLoc == null) {
return false;
}
ProgramSelection progSel = codeViewerService.getCurrentSelection();
ProgramSelection traceSel = listingService.getCurrentSelection();
if (progSel != null && progSel.getNumAddressRanges() > 1) {
return false;
}
if (traceSel != null && traceSel.getNumAddressRanges() > 1) {
return false;
}
long progLen = progSel == null ? 0 : progSel.getNumAddresses();
long traceLen = traceSel == null ? 0 : traceSel.getNumAddresses();
if (progLen == 0 && traceLen == 0) {
return false;
}
return true;
}
private void activatedAdd(ActionContext ignore) {
// TODO: Use events to track selections/locations
tool.showDialog(addMappingDialog);
// Try to populate the dialog based on selection, if applicable
if (codeViewerService == null || listingService == null) {
return;
}
@@ -303,17 +286,13 @@ public class DebuggerStaticMappingProvider extends ComponentProviderAdapter
Address progStart = progLen != 0 ? progSel.getMinAddress() : progLoc.getAddress();
Address traceStart = traceLen != 0 ? traceSel.getMinAddress() : traceLoc.getAddress();
TraceProgramView view = (TraceProgramView) traceLoc.getProgram();
TraceLocation from =
new DefaultTraceLocation(currentTrace, null, Range.atLeast(view.getSnap()), traceStart);
ProgramLocation to = new ProgramLocation(progLoc.getProgram(), progStart);
try (UndoableTransaction tid =
UndoableTransaction.start(currentTrace, "Add Static Mapping", false)) {
mappingService.addMapping(from, to, length, true);
tid.commit();
try {
addMappingDialog.setValues(progLoc.getProgram(), currentTrace, progStart, traceStart,
length, Range.atLeast(view.getSnap()));
}
catch (TraceConflictedMappingException e) {
throw new AssertionError(e); // I said truncateExisting
catch (AddressOverflowException e) {
Msg.showError(this, null, "Add Mapping", "Error populating dialog");
}
}
@@ -387,5 +366,11 @@ public class DebuggerStaticMappingProvider extends ComponentProviderAdapter
currentTrace = trace;
addNewListeners();
loadMappings();
addMappingDialog.setTrace(trace);
}
public void setProgram(Program program) {
addMappingDialog.setProgram(program);
}
}

View File

@@ -15,6 +15,7 @@
*/
package ghidra.app.plugin.core.debug.gui.modules;
import java.math.BigInteger;
import java.net.URL;
import com.google.common.collect.Range;
@@ -24,6 +25,7 @@ import ghidra.trace.model.Trace;
import ghidra.trace.model.modules.TraceStaticMapping;
public class StaticMappingRow {
private static final BigInteger BIT64 = BigInteger.ONE.shiftLeft(64);
private final TraceStaticMapping mapping;
public StaticMappingRow(TraceStaticMapping mapping) {
@@ -54,6 +56,19 @@ public class StaticMappingRow {
return mapping.getLength();
}
public BigInteger getBigLength() {
long length = mapping.getLength();
if (length == 0) {
return BIT64;
}
else if (length < 0) {
return BigInteger.valueOf(length).add(BIT64);
}
else {
return BigInteger.valueOf(length);
}
}
public long getShift() {
return mapping.getShift();
}

View File

@@ -940,31 +940,36 @@ public class DebuggerStaticMappingServicePlugin extends Plugin
if (toURL == null) {
noProject();
}
try {
Address start = from.getAddress();
Address end = start.addNoWrap(length - 1);
// Also check end in the destination
Address toAddress = to.getByteAddress();
toAddress.addNoWrap(length - 1); // Anticipate possible AddressOverflow
AddressRangeImpl range = new AddressRangeImpl(start, end);
if (truncateExisting) {
long truncEnd = DBTraceUtils.lowerEndpoint(from.getLifespan()) - 1;
for (TraceStaticMapping existing : List
.copyOf(manager.findAllOverlapping(range, from.getLifespan()))) {
existing.delete();
if (Long.compareUnsigned(existing.getStartSnap(), truncEnd) < 0) {
manager.add(existing.getTraceAddressRange(),
Range.closed(existing.getStartSnap(), truncEnd),
existing.getStaticProgramURL(), existing.getStaticAddress());
}
Address fromAddress = from.getAddress();
Address toAddress = to.getByteAddress();
long maxFromLengthMinus1 =
fromAddress.getAddressSpace().getMaxAddress().subtract(fromAddress);
long maxToLengthMinus1 =
toAddress.getAddressSpace().getMaxAddress().subtract(toAddress);
if (Long.compareUnsigned(length - 1, maxFromLengthMinus1) > 0) {
throw new IllegalArgumentException("Length would cause address overflow in trace");
}
if (Long.compareUnsigned(length - 1, maxToLengthMinus1) > 0) {
throw new IllegalArgumentException("Length would cause address overflow in program");
}
Address end = fromAddress.addWrap(length - 1);
// Also check end in the destination
AddressRangeImpl range = new AddressRangeImpl(fromAddress, end);
Range<Long> fromLifespan = from.getLifespan();
if (truncateExisting) {
long truncEnd = DBTraceUtils.lowerEndpoint(fromLifespan) - 1;
for (TraceStaticMapping existing : List
.copyOf(manager.findAllOverlapping(range, fromLifespan))) {
existing.delete();
if (fromLifespan.hasLowerBound() &&
Long.compare(existing.getStartSnap(), truncEnd) <= 0) {
manager.add(existing.getTraceAddressRange(),
Range.closed(existing.getStartSnap(), truncEnd),
existing.getStaticProgramURL(), existing.getStaticAddress());
}
}
manager.add(range, from.getLifespan(), toURL,
toAddress.toString(true));
}
catch (AddressOverflowException e) {
throw new IllegalArgumentException("Length would cause address overflow", e);
}
manager.add(range, fromLifespan, toURL, toAddress.toString(true));
}
@Override

View File

@@ -535,7 +535,7 @@ public interface DebuggerStaticMappingService {
*
* @param from the source trace location, including lifespan
* @param to the destination program location
* @param length the length of the mapped region
* @param length the length of the mapped region, where 0 indicates {@code 1 << 64}.
* @param truncateExisting true to delete or truncate the lifespan of overlapping entries
* @throws TraceConflictedMappingException if a conflicting mapping overlaps the source and
* {@code truncateExisting} is false.

View File

@@ -106,7 +106,7 @@ public class DebuggerStaticMappingProviderTest extends AbstractGhidraHeadedDebug
@Test
public void testAddAction() throws Exception {
assertFalse(mappingsProvider.actionAdd.isEnabled());
assertTrue(mappingsProvider.actionAdd.isEnabled());
createProgramFromTrace(tb.trace);
intoProject(tb.trace);
@@ -135,16 +135,18 @@ public class DebuggerStaticMappingProviderTest extends AbstractGhidraHeadedDebug
programManager.openProgram(program);
waitForSwing();
assertFalse(mappingsProvider.actionAdd.isEnabled());
ProgramSelection traceSel =
new ProgramSelection(tb.addr(0xdeadbeefL), tb.addr(0xdeadbeefL + 0x0f));
listingPlugin.getProvider().setSelection(traceSel);
codeViewerPlugin.goTo(new ProgramLocation(program, addr(program, 0xc0de1234L)), true);
waitForSwing();
assertTrue(mappingsProvider.actionAdd.isEnabled());
performAction(mappingsProvider.actionAdd, true);
performAction(mappingsProvider.actionAdd, false);
DebuggerAddMappingDialog dialog = waitForDialogComponent(DebuggerAddMappingDialog.class);
dialog.applyCallback();
dialog.close();
waitForDomainObject(tb.trace);
TraceStaticMapping entry = Unique.assertOne(manager.getAllEntries());
assertEquals(Range.atLeast(0L), entry.getLifespan());