Merge remote-tracking branch 'origin/GP-5893_ghidra_red_CallTreeViewer'

This commit is contained in:
Ryan Kurtz
2026-05-27 08:14:19 -04:00
34 changed files with 4739 additions and 69 deletions

View File

@@ -15,9 +15,12 @@
*/
package ghidra.app.services;
import java.util.List;
import ghidra.app.util.viewer.listingpanel.ListingPanel;
import ghidra.debug.api.action.AutoReadMemorySpec;
import ghidra.debug.api.action.LocationTrackingSpec;
import ghidra.debug.api.listing.DebuggerListing;
import ghidra.debug.api.listing.MultiBlendedListingBackgroundColorModel;
import ghidra.framework.plugintool.ServiceInfo;
import ghidra.program.model.address.Address;
@@ -57,6 +60,20 @@ public interface DebuggerListingService extends CodeViewerService {
*/
LocationTrackingSpec getTrackingSpec();
/**
* Create a new disconnected debugger listing
*
* @return newly created debugger listing
*/
DebuggerListing createNewListing();
/**
* Get all debugger listings
*
* @return List of all debugger listings
*/
List<DebuggerListing> getAllListings();
/**
* Get the auto-read memory specification of the main listing.
*
@@ -108,4 +125,11 @@ public interface DebuggerListingService extends CodeViewerService {
*/
MultiBlendedListingBackgroundColorModel createListingBackgroundColorModel(
ListingPanel listingPanel);
/**
* Get the next custom title for a DebuggerListing
*
* @return Next custom title
*/
String findNextCustomTitle();
}

View File

@@ -4,9 +4,9 @@
* 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.
@@ -52,7 +52,8 @@ import ghidra.util.task.TaskMonitor;
* can determine how to present them.
*/
@ServiceInfo(
defaultProviderName = "ghidra.app.plugin.core.debug.service.progress.ProgressServicePlugin")
defaultProviderName = "ghidra.app.plugin.core.debug.service.progress.ProgressServicePlugin"
)
public interface ProgressService {
/**
* Publish a task and create a monitor for it
@@ -150,6 +151,9 @@ public interface ProgressService {
monitor.setCancelEnabled(canCancel);
CompletableFuture<T> future = futureSupplier.apply(monitor);
future.handle((t, ex) -> {
if (canCancel) {
monitor.cancel();
}
monitor.close();
return null;
});

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.debug.api.listing;
import ghidra.app.nav.Navigatable;
import ghidra.debug.api.action.LocationTrackingSpec;
public interface DebuggerListing extends Navigatable {
/**
* Get the window title of this debugger listing
*
* @return Title of window
*/
String getTitle();
/**
* Returns boolean if this listing is the main debugger listing
*
* @return true/false if this is the main listing
*/
boolean isMainListing();
/**
* Set a custom title.
* <p>
* Setting the title here prevents future calls to
* {@link docking.ComponentProvider#setTitle(String)} from having any effect. This is done to
* preserve the custom
* title.
*
* @param title the title
*/
void setCustomTitle(String title);
/**
* Set if this debugger listing should follow the current thread when displaying
*
* @param follows true/false if this listing should follow the current thread
*/
void setFollowsCurrentThread(boolean follows);
/**
* Set what this debugger listing should track as the user performs actions
*
* @param spec {@link LocationTrackingSpec} describing how/what the listing will track
*/
void setTrackingSpec(LocationTrackingSpec spec);
}

View File

@@ -4,9 +4,9 @@
* 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.
@@ -22,17 +22,23 @@ import ghidra.trace.model.modules.TraceModule;
public class DebuggerMissingModuleActionContext extends DefaultActionContext {
private final TraceModule module;
private final long snap;
private final int hashCode;
public DebuggerMissingModuleActionContext(TraceModule module) {
public DebuggerMissingModuleActionContext(TraceModule module, long snap) {
this.module = Objects.requireNonNull(module);
this.hashCode = Objects.hash(getClass(), module);
this.hashCode = Objects.hash(getClass(), module, snap);
this.snap = snap;
}
public TraceModule getModule() {
return module;
}
public long getSnap() {
return snap;
}
@Override
public int hashCode() {
return hashCode;
@@ -49,6 +55,9 @@ public class DebuggerMissingModuleActionContext extends DefaultActionContext {
if (!this.module.equals(that.module)) {
return false;
}
if (this.snap != that.snap) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,600 @@
/* ###
* 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.util.opinion;
import java.io.*;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jdom2.JDOMException;
import db.Transaction;
import ghidra.app.util.Option;
import ghidra.app.util.bin.ByteProvider;
import ghidra.app.util.importer.MessageLog;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainObject;
import ghidra.program.database.ProgramContentHandler;
import ghidra.program.model.address.*;
import ghidra.program.model.lang.*;
import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.RefType;
import ghidra.program.model.symbol.SourceType;
import ghidra.program.util.AddressEvaluator;
import ghidra.trace.database.DBTrace;
import ghidra.trace.database.target.DBTraceObjectManager;
import ghidra.trace.model.Lifespan;
import ghidra.trace.model.Trace;
import ghidra.trace.model.memory.TraceMemoryFlag;
import ghidra.trace.model.modules.TraceModule;
import ghidra.trace.model.modules.TraceModuleManager;
import ghidra.trace.model.stack.TraceStackFrame;
import ghidra.trace.model.target.TraceObject;
import ghidra.trace.model.target.TraceObject.ConflictResolution;
import ghidra.trace.model.target.TraceObjectManager;
import ghidra.trace.model.target.TraceObjectManager.BypassWriteCache;
import ghidra.trace.model.target.path.KeyPath;
import ghidra.trace.model.target.schema.*;
import ghidra.trace.model.target.schema.TraceObjectSchema.SchemaName;
import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.model.time.TraceSnapshot;
import ghidra.util.Msg;
import ghidra.util.NumericUtilities;
import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor;
public class TenetPlusPlusLoader implements Loader {
private record ModuleEvent(String path, String module, AddressRange range) {}
private static final String TRACE_SUFFIX = ".trace";
private static final String TENET_SUFFIX = ".tenet";
private static final Set<String> SUFFIXES = Set.of(TRACE_SUFFIX, TENET_SUFFIX);
private static final Pattern TID_PATTERN = Pattern.compile("tid=([0-9a-fA-F]+),");
private static final Pattern REG_PATTERN =
Pattern.compile("(?:^|,)(?!(?:mr|mw|ma|slide)=)([a-zA-Z0-9]+)=0x([0-9a-fA-F]+)");
private static final Pattern MEM_PATTERN =
Pattern.compile("(?:^|,)(mr|mw|ma)=0x([^:]+):([0-9a-fA-F]+)");
private static final Pattern MODULE_LOAD_PATTERN =
Pattern.compile("Loaded image: 0x([0-9a-fA-F]+):0x([0-9a-fA-F]+) -> (.*)$");
private static final Pattern MODULE_UNLOAD_PATTERN =
Pattern.compile("Unloaded image: 0x([0-9a-fA-F]+):0x([0-9a-fA-F]+) -> (.*)$");
static final int ERROR_THRESHOLD = 10;
static final String DOMAIN_FILE_OPTION_NAME = "Program to associate trace file with";
private static final String TENET_PLUS_PLUS_CTX_XML = """
<context>
<schema name='TenetPlusPlusSession' canonical='yes'>
<interface name='Aggregate'/>
<interface name='Process'/>
<attribute name='Threads' schema='ThreadContainer' />
<attribute name='Memory' schema='Memory'/>
<attribute name='Breakpoints' schema='BreakpointContainer'/>
<attribute name='Modules' schema='ModuleContainer'/>
</schema>
<schema name='ThreadContainer' canonical='yes'>
<element schema='Thread' />
</schema>
<schema name='Thread'>
<interface name='Aggregate'/>
<interface name='Thread'/>
<interface name='ExecutionStateful'/>
<attribute name='Stack' schema='Stack'/>
</schema>
<schema name='Stack' canonical='yes'>
<interface name='Stack'/>
<interface name='Aggregate'/>
<element schema='Frame'/>
</schema>
<schema name='Frame'>
<interface name='Aggregate'/>
<interface name='StackFrame'/>
<attribute name='Registers' schema='RegisterContainer'/>
</schema>
<schema name='RegisterContainer'>
<interface name='RegisterContainer'/>
<element schema='Register'/>
</schema>
<schema name='Register'>
<interface name='Register'/>
</schema>
<schema name='Memory' canonical='yes'>
<interface name='Memory'/>
<element schema='MemoryRegion'/>
</schema>
<schema name='MemoryRegion'>
<interface name='MemoryRegion'/>
</schema>
<schema name='ModuleContainer' canonical='yes'>
<element schema='Module'/>
</schema>
<schema name='Module'>
<interface name='Module'/>
<attribute name='Sections' schema='SectionContainer'/>
</schema>
<schema name='SectionContainer' canonical='yes'>
<element schema='Section'/>
</schema>
<schema name='Section'>
<interface name='Section'/>
</schema>
<schema name='BreakpointContainer' canonical='yes'>
<element schema='Breakpoint'/>
</schema>
<schema name='Breakpoint'>
<interface name='BreakpointSpec'/>
<interface name='BreakpointLocation'/>
</schema>
</context>
""";
private static final SchemaContext SCHEMA_CTX;
private static final TraceObjectSchema TENET_PLUS_PLUS_SESSION_SCHEMA;
static {
try {
SCHEMA_CTX = XmlSchemaContext.deserialize(TENET_PLUS_PLUS_CTX_XML);
}
catch (final JDOMException e) {
throw new AssertionError(e);
}
TENET_PLUS_PLUS_SESSION_SCHEMA =
SCHEMA_CTX.getSchema(new SchemaName("TenetPlusPlusSession"));
}
private String STORE_REGS_AS_ATTRS_OPTION_NAME = "Store registers as attributes in model";
private boolean storeRegsAsAttrs = false;
private String ADD_ALL_MEM_REFS_OPTION_NAME = "Add all memory references";
private boolean addAllMemRefs = false;
private static boolean isTenetPlusPlusFile(final ByteProvider provider) {
final String nameLower = provider.getName().toLowerCase();
if (!SUFFIXES.stream().anyMatch(nameLower::endsWith)) {
return false;
}
try {
final byte[] bytes = new byte[1000];
provider.getInputStream(0).read(bytes);
final String[] lines = new String(bytes, StandardCharsets.UTF_8).split("\n");
// Skip last line as it could be a partial one
for (int i = 0; i < (lines.length - 1); i++) {
final String line = lines[i];
if ((!MODULE_LOAD_PATTERN.matcher(line).find() &&
!MODULE_UNLOAD_PATTERN.matcher(line).find()) &&
(!TID_PATTERN.matcher(line).find() && !REG_PATTERN.matcher(line).find())) {
return false;
}
}
return true;
}
catch (final IOException e) {
return false;
}
}
private AddressSpace defaultSpace;
private Program program;
private void addModuleEventsToSnap(Trace trace, Long snap, List<ModuleEvent> load,
List<ModuleEvent> unload) throws DuplicateNameException {
final TraceModuleManager modMan = trace.getModuleManager();
if (!unload.isEmpty()) {
for (final ModuleEvent event : unload) {
final TraceModule module = modMan.getLoadedModuleByPath(snap, event.path());
if (module != null) {
module.remove(snap);
}
}
unload.clear();
}
if (!load.isEmpty()) {
for (final ModuleEvent event : load) {
modMan.addLoadedModule(event.path, event.module, event.range, snap);
}
load.clear();
}
}
/**
* Create an address in the processor's default space.
*
* @param offset the byte offset
* @return the address
*/
private Address addr(final long offset) {
return defaultSpace.getAddress(offset);
}
@Override
public Collection<LoadSpec> findSupportedLoadSpecs(final ByteProvider provider)
throws IOException {
final List<LoadSpec> loadSpecs = new ArrayList<>();
if (isTenetPlusPlusFile(provider)) {
// TODO: This LanguageCompilerSpecPair is not used by the loader
// just putting a dummy value in so the user doesn't have to
loadSpecs.add(new LoadSpec(this, 0,
new LanguageCompilerSpecPair("DATA:LE:64:default", "pointer64"), true));
}
return loadSpecs;
}
@Override
public List<Option> getDefaultOptions(final ByteProvider provider, final LoadSpec loadSpec,
final DomainObject domainObject, final boolean loadIntoProgram,
final boolean mirrorFsLayout) {
final List<Option> list = new ArrayList<>();
list.add(Option.newDomainFile(DOMAIN_FILE_OPTION_NAME)
.commandLineArgument(createArg("-associatedProgram"))
.build());
list.add(Option.newBoolean(STORE_REGS_AS_ATTRS_OPTION_NAME)
.commandLineArgument("-storeRegs")
.value(storeRegsAsAttrs)
.build());
list.add(Option.newBoolean(ADD_ALL_MEM_REFS_OPTION_NAME)
.commandLineArgument("-allRefs")
.value(addAllMemRefs)
.build());
return list;
}
@Override
public String getName() {
return "Tenet++ Trace Format";
}
@Override
public LoaderTier getTier() {
return LoaderTier.GENERIC_TARGET_LOADER;
}
@Override
public int getTierPriority() {
return 0;
}
@Override
public LoadResults<? extends DomainObject> load(final ImporterSettings settings)
throws IOException, CancelledException, VersionException, LoadException {
Trace trace;
String programPath = null;
for (final Option option : settings.options()) {
final String name = option.getName();
if (name.equals(DOMAIN_FILE_OPTION_NAME)) {
programPath = (String) option.getValue();
}
else if (name.equals(STORE_REGS_AS_ATTRS_OPTION_NAME)) {
storeRegsAsAttrs = (boolean) option.getValue();
}
else if (name.equals(ADD_ALL_MEM_REFS_OPTION_NAME)) {
addAllMemRefs = (boolean) option.getValue();
}
}
if ((programPath == null) || programPath.isBlank()) {
throw new LoadException("No progam to associate with trace was given");
}
Msg.info(this, "Loading trace for %s".formatted(programPath));
final DomainFile df = settings.project().getProjectData().getFile(programPath);
if (!df.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
throw new LoadException("Selected file is not a program");
}
Program program = null;
try {
program = (Program) df.getDomainObject(this, false, false, settings.monitor());
final long start = System.currentTimeMillis();
trace = loadTrace(settings.provider(), settings.importName(), program,
settings.consumer(), settings.log(), settings.monitor());
final long loadDone = System.currentTimeMillis();
// TODO: This is needed because
// ghidra.trace.database.DBTraceContentHandler.createFile can't do
// it, due to a lock in
// ghidra.framework.data.GhidraFolderData.createFile
final DBTraceObjectManager objManager = (DBTraceObjectManager) trace.getObjectManager();
objManager.flushWbCaches();
final long flushDone = System.currentTimeMillis();
settings.log()
.appendMsg("%d ms to load trace | %d ms to flush cache | %d ms total time"
.formatted(loadDone - start, flushDone - loadDone, flushDone - start));
return new LoadResults<>(new Loaded<>(trace, settings));
}
finally {
if (program != null) {
program.release(this);
}
}
}
@Override
public void loadInto(final Program program, final ImporterSettings settings)
throws IOException, LoadException, CancelledException {
throw new LoadException("Cannot add Trace to a program");
}
private Trace loadTrace(final ByteProvider provider, final String name, final Program program,
final Object consumer, final MessageLog log, final TaskMonitor monitor)
throws LanguageNotFoundException, IOException, CancelledException {
this.program = program;
final Language lang = program.getLanguage();
defaultSpace = lang.getAddressFactory().getDefaultAddressSpace();
final Trace trace = new DBTrace(name, program.getCompilerSpec(), consumer);
final TraceObjectManager om = trace.getObjectManager();
try (Transaction tx = trace.openTransaction("Import Tenet++ Trace: %s".formatted(name));
BypassWriteCache bypass = om.withoutWriteCache()) {
om.createRootObject(TENET_PLUS_PLUS_SESSION_SCHEMA);
om.createObject(KeyPath.parse("Breakpoints"))
.insert(Lifespan.ALL, ConflictResolution.DENY);
om.createObject(KeyPath.parse("Threads")).insert(Lifespan.ALL, ConflictResolution.DENY);
final Pattern ipPattern = Pattern.compile(
"(?:^|,)%s=0x([0-9a-fA-F]+)".formatted(lang.getProgramCounter().getName()),
Pattern.CASE_INSENSITIVE);
final BufferedReader lineCounter = new BufferedReader(
new InputStreamReader(provider.getInputStream(0), StandardCharsets.UTF_8));
final long numLines = lineCounter.lines().count();
lineCounter.close();
final Map<String, TraceThread> tidToThreadMap = new HashMap<>();
final List<ModuleEvent> modulesToLoad = new ArrayList<>();
final List<ModuleEvent> modulesToUnload = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(provider.getInputStream(0), StandardCharsets.UTF_8))) {
long curIp = 0;
long lineNumber = 1;
int errorCount = 0;
long snapNumber = 0;
String line = reader.readLine();
try {
do {
monitor.checkCancelled();
if (parseModuleEvents(log, line, modulesToLoad, modulesToUnload)) {
// Module load/unload no need to continue parsing
lineNumber++;
line = reader.readLine();
continue;
}
final Matcher tidMatcher = TID_PATTERN.matcher(line);
final Matcher ipMatcher = ipPattern.matcher(line);
if (!tidMatcher.find()) {
log.appendMsg(
"Line %d: Unable to find TID, skipping...".formatted(lineNumber));
errorCount++;
}
else if (!ipMatcher.find()) {
log.appendMsg(
"Line %d: Unable to find PC, skipping...".formatted(lineNumber));
errorCount++;
}
else {
final TraceSnapshot snapshot = trace.getTimeManager()
.createSnapshot("Snapshot %d".formatted(snapNumber++));
final Long snap = snapshot.getKey();
final String tid = tidMatcher.group(1);
if (!tidToThreadMap.containsKey(tid)) {
final TraceThread thread = trace.getThreadManager()
.createThread("Threads[%s]".formatted(tid),
"Threads[%s]".formatted(tid), snap);
tidToThreadMap.put(tid, thread);
}
final TraceThread traceThread = tidToThreadMap.get(tid);
snapshot.setEventThread(traceThread);
if (snap == 0) {
trace.getMemoryManager()
.addRegion("Memory[ALL]", Lifespan.nowOn(0),
rng(0x0, 0xFFFF_FFFF_FFFF_FFFFL), TraceMemoryFlag.READ,
TraceMemoryFlag.EXECUTE, TraceMemoryFlag.WRITE);
}
addModuleEventsToSnap(trace, snap, modulesToLoad, modulesToUnload);
curIp = Long.parseLong(ipMatcher.group(1), 16);
if (parseRegisterOperations(log, trace, traceThread, snap, curIp, line,
lineNumber, numLines, monitor)) {
parseMemoryOperations(trace, snap, curIp, line, lineNumber,
numLines, monitor);
}
else {
errorCount++;
}
}
if (errorCount >= ERROR_THRESHOLD) {
throw new LoadException("Encountered too many errors with this trace");
}
lineNumber++;
line = reader.readLine();
}
while (line != null);
}
catch (final CancelledException e) {
throw e;
}
catch (final Exception e) {
throw new LoadException(e);
}
}
}
return trace;
}
private void parseMemoryOperations(final Trace trace, final long snap, final long curIp,
final String line, long lineNumber, long numLines, final TaskMonitor monitor)
throws Exception {
final Matcher memMatcher = MEM_PATTERN.matcher(line);
monitor.initialize(memMatcher.results().count(),
"Parsing memory operations for line %d/%d".formatted(lineNumber, numLines));
memMatcher.reset();
// TODO Speed this up
while (memMatcher.find()) {
monitor.increment();
final RefType refType = switch (memMatcher.group(1)) {
case "mr" -> RefType.READ;
case "mw" -> RefType.WRITE;
default -> RefType.READ_WRITE;
};
final Address address = toAddr(memMatcher.group(2));
final byte[] bytes = NumericUtilities.convertStringToBytes(memMatcher.group(3));
// TODO: Should we do this?
// If it's a read/write for the same address we're gonna assume it's
// an aggregation of memory operations, so we can't say for certain
// that the current instruction performed both operations
if (addAllMemRefs || (refType != RefType.READ_WRITE)) {
trace.getReferenceManager()
.addMemoryReference(Lifespan.at(snap), addr(curIp),
new AddressRangeImpl(address, bytes.length), refType,
SourceType.IMPORTED, -1);
}
trace.getMemoryManager().putBytes(snap, address, ByteBuffer.wrap(bytes));
}
}
private boolean parseModuleEvents(final MessageLog log, String line, List<ModuleEvent> load,
List<ModuleEvent> unload) {
final Matcher modLoadMatcher = MODULE_LOAD_PATTERN.matcher(line);
if (modLoadMatcher.find()) {
log.appendMsg("Found load of %s @ %s:%s".formatted(modLoadMatcher.group(3),
modLoadMatcher.group(2), modLoadMatcher.group(1)));
final String mod = modLoadMatcher.group(3).replace("[", "").replace("]", "");
load.add(new ModuleEvent("Modules[%s]".formatted(mod), mod,
rng(Long.parseLong(modLoadMatcher.group(1), 16),
Long.parseLong(modLoadMatcher.group(2), 16))));
return true;
}
final Matcher modUnloadMatcher = MODULE_UNLOAD_PATTERN.matcher(line);
if (modUnloadMatcher.find()) {
log.appendMsg("Found unload of %s @ %s:%s".formatted(modUnloadMatcher.group(3),
modUnloadMatcher.group(2), modUnloadMatcher.group(1)));
final String mod = modUnloadMatcher.group(3).replace("[", "").replace("]", "");
unload.add(new ModuleEvent("Modules[%s]".formatted(mod), mod,
rng(Long.parseLong(modUnloadMatcher.group(1), 16),
Long.parseLong(modUnloadMatcher.group(2), 16))));
return true;
}
return false;
}
private boolean parseRegisterOperations(final MessageLog log, final Trace trace,
final TraceThread traceThread, final long snap, final long curIp, final String line,
final long lineNumber, long numLines, final TaskMonitor monitor) throws Exception {
final TraceStackFrame frame =
trace.getStackManager().getStack(traceThread, snap, true).getFrame(snap, 0, true);
frame.setProgramCounter(Lifespan.nowOn(snap), addr(curIp));
final KeyPath traceRegistersPath = frame.getObject().getCanonicalPath().extend("Registers");
final TraceObject traceRegisters =
trace.getObjectManager().createObject(traceRegistersPath);
traceRegisters.insert(Lifespan.ALL, ConflictResolution.DENY);
final Matcher regMatcher = REG_PATTERN.matcher(line);
monitor.initialize(regMatcher.results().count(),
"Parsing register operations for line %d/%d".formatted(lineNumber, numLines));
regMatcher.reset();
while (regMatcher.find()) {
monitor.increment();
final BigInteger val = new BigInteger(regMatcher.group(2), 16);
final Register regObj = trace.getProgramView().getRegister(regMatcher.group(1));
if (regObj == null) {
log.appendMsg("Line %d: Register %s not found in program language!"
.formatted(lineNumber, regMatcher.group(1)));
return false;
}
if (storeRegsAsAttrs) {
traceRegisters.setElement(Lifespan.nowOn(snap), regObj.getName(), val.longValue());
}
trace.getMemoryManager()
.getMemoryRegisterSpace(frame, true)
.setValue(snap, new RegisterValue(regObj, val));
}
return true;
}
/**
* Create an address range in the processor's default space.
*
* @param min the minimum byte offset
* @param max the maximum (inclusive) byte offset
* @return the range
*/
private AddressRange rng(final long min, final long max) {
return new AddressRangeImpl(addr(min), addr(max));
}
private Address toAddr(final String addressString) {
return AddressEvaluator.evaluate(program, addressString);
}
@Override
public String validateOptions(final ByteProvider provider, final LoadSpec loadSpec,
final List<Option> options, final Program program) {
if (options != null) {
for (final Option option : options) {
final String name = option.getName();
if (name.equals(DOMAIN_FILE_OPTION_NAME) &&
!String.class.isAssignableFrom(option.getValueClass())) {
return "Invalid type for option: " + name + " - " + option.getValueClass();
}
}
}
return null;
}
}

View File

@@ -0,0 +1,393 @@
/* ###
* 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.util.opinion;
import static org.junit.Assert.*;
import java.nio.ByteBuffer;
import java.util.List;
import org.junit.Ignore;
import org.junit.Test;
import generic.Unique;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin;
import ghidra.app.plugin.core.debug.gui.model.DebuggerModelPlugin;
import ghidra.app.plugin.core.debug.gui.register.DebuggerRegistersPlugin;
import ghidra.app.plugin.core.debug.gui.thread.DebuggerThreadsPlugin;
import ghidra.app.plugin.core.debug.gui.time.DebuggerTimePlugin;
import ghidra.app.util.Option;
import ghidra.app.util.bin.ByteArrayProvider;
import ghidra.app.util.bin.ByteProvider;
import ghidra.app.util.importer.MessageLog;
import ghidra.app.util.opinion.Loader.ImporterSettings;
import ghidra.framework.model.DomainObject;
import ghidra.program.model.address.AddressSpace;
import ghidra.trace.model.Trace;
import ghidra.trace.model.memory.TraceMemorySpace;
import ghidra.trace.model.thread.TraceThread;
public class TenetLoaderPlusPlusTest extends AbstractGhidraHeadedDebuggerTest {
/**
* Create a byte array
*
* <p>
* This is basically syntactic sugar, since expressing a byte array literal can get obtuse in
* Java. {@code new byte[] {0, 1, 2, (byte) 0x80, (byte) 0xff}} vs
* {@code arr(0, 1, 2, 0x80, 0xff)}.
*
* @param e the bytes' values
* @return the array
*/
private byte[] arr(final int... e) {
final byte[] result = new byte[e.length];
for (int i = 0; i < e.length; i++) {
result[i] = (byte) e[i];
}
return result;
}
private Trace createTraceWithLoader(final String testFile) throws Exception {
this.createProgram();
intoProject(program);
final TenetPlusPlusLoader loader = new TenetPlusPlusLoader();
final ByteProvider provider =
new ByteArrayProvider("test.tenet", testFile.getBytes("utf8"));
final LoadSpec loadSpec = Unique.assertOne(loader.findSupportedLoadSpecs(provider));
final List<Option> options =
loader.getDefaultOptions(provider, loadSpec, null, false, false);
final Option programOption = Unique.assertOne(
options.stream().filter(o -> o.getName().equals(TenetLoader.DOMAIN_FILE_OPTION_NAME)));
programOption.setValue(program.getDomainFile().getPathname());
final MessageLog log = new MessageLog();
final ImporterSettings settings = new ImporterSettings(provider, "test", env.getProject(),
"/", false, loadSpec, options, this, log, monitor);
final LoadResults<? extends DomainObject> results = loader.load(settings);
if (!(results.getPrimary().domainObject instanceof final Trace trace)) {
fail("Import result was not a trace");
return null;
}
return trace;
}
/*
* Test that the loader continues on a bad register name
*/
@Test
public void testBadRegisterNames() throws Exception {
final String testFile = """
tid=1,pc=0x1,rax=0x1234
tid=1,pc=0x2,rax=0x1234
tid=1,pc=0x3,rax=0x1234
tid=1,pc=0x4
tid=1,pc=0x5
tid=1,pc=0x6,rax=0x1234
tid=1,pc=0x7,rax=0x1234
tid=1,pc=0x8,rax=0x1234
tid=1,pc=0x9,rax=0x1234
tid=1,pc=0xa,rax=0x1234
""";
final Trace trace = createTraceWithLoader(testFile);
assertNotNull(trace);
}
/*
* Test the file content check for the loader
*/
@Test
public void testFileContent() throws Exception {
final String junk_line = "asdf\n";
final String module_load_line = "Loaded image: 0x01:0x1337 -> MyImage\n";
final String reg_line = "tid=1,pc=0x1,r0=0x1234\n";
final String reg_line_no_tid = "pc=0x1,r0=0x1234\n";
this.createProgram();
intoProject(program);
final TenetLoader loader = new TenetLoader();
ByteProvider provider = new ByteArrayProvider("test.tenet", (junk_line).getBytes("utf-8"));
assertEquals(0, loader.findSupportedLoadSpecs(provider).size());
provider =
new ByteArrayProvider("test.tenet", (module_load_line + junk_line).getBytes("utf-8"));
assertEquals(0, loader.findSupportedLoadSpecs(provider).size());
provider = new ByteArrayProvider("test.tenet", (reg_line + junk_line).getBytes("utf-8"));
assertEquals(0, loader.findSupportedLoadSpecs(provider).size());
provider = new ByteArrayProvider("test.tenet",
(module_load_line + reg_line.repeat(TenetPlusPlusLoader.ERROR_THRESHOLD - 2) +
junk_line).getBytes("utf-8"));
assertEquals(0, loader.findSupportedLoadSpecs(provider).size());
provider = new ByteArrayProvider("test.tenet",
reg_line_no_tid.repeat(TenetPlusPlusLoader.ERROR_THRESHOLD).getBytes("utf-8"));
assertFalse(loader.findSupportedLoadSpecs(provider)
.stream()
.anyMatch(c -> c.getClass().equals(TenetPlusPlusLoader.class)));
}
/*
* Test the filename check for the loader
*/
@Test
public void testFileName() throws Exception {
final String testFile = """
tid=1,pc=0x1,r0=0x1234
tid=1,pc=0x2,r1=0x4321
tid=1,pc=0x3,r1=0x4321
tid=1,pc=0x4,r1=0x4321
tid=1,pc=0x5,r1=0x4321
tid=1,pc=0x6,r1=0x4321
tid=1,pc=0x7,r1=0x4321
tid=1,pc=0x8,r1=0x4321
tid=1,pc=0x9,r1=0x4321
tid=1,pc=0xa,r1=0x4321
""";
this.createProgram();
intoProject(program);
final TenetLoader loader = new TenetLoader();
ByteProvider provider = new ByteArrayProvider("test.tenet", testFile.getBytes("utf8"));
Unique.assertOne(loader.findSupportedLoadSpecs(provider));
provider = new ByteArrayProvider("test.trace", testFile.getBytes("utf8"));
Unique.assertOne(loader.findSupportedLoadSpecs(provider));
provider = new ByteArrayProvider("test.anythingelse", testFile.getBytes("utf8"));
assertEquals(0, loader.findSupportedLoadSpecs(provider).size());
}
/*
* Automate loading a trace for manual testing purposes
*/
@Ignore
public void testManual() throws Exception {
final String testFile = """
tid=1,pc=0x1,r0=0x1234
tid=1,pc=0x2,r1=0x4321
tid=1,pc=0x3,r1=0x4321
tid=1,pc=0x4,r1=0x4321
tid=1,pc=0x5,r1=0x4321
tid=1,pc=0x6,r1=0x4321
tid=1,pc=0x7,r1=0x4321
tid=1,pc=0x8,r1=0x4321
tid=1,pc=0x9,r1=0x4321
tid=1,pc=0xa,r1=0x4321
""";
addPlugin(tool, DebuggerRegistersPlugin.class);
addPlugin(tool, DebuggerModelPlugin.class);
addPlugin(tool, DebuggerThreadsPlugin.class);
addPlugin(tool, DebuggerListingPlugin.class);
addPlugin(tool, DebuggerTimePlugin.class);
final Trace trace = createTraceWithLoader(testFile);
traceManager.openTrace(trace);
traceManager.activateTrace(trace);
Thread.sleep(1000);
}
/*
* Test that the loader after more than {@link TenetLoader.ERROR_THRESHOLD}
* lines with bad register names
*/
@Test
public void testManyLinesBadRegisterNames() throws Exception {
// Should fail after 10 lines with bad registers
final String badTestFile =
"tid=1,pc=0x1,rax=0x1234\n".repeat(TenetLoader.ERROR_THRESHOLD + 1);
assertThrows(LoadException.class, () -> createTraceWithLoader(badTestFile));
}
/*
* Test that the loader fails after {@TenetLoader.ERROR_THRESHOLD} lines
* without a PC
*/
@Test
public void testManyLinesNoPc() throws Exception {
// Should fail after {@TenetLoader.ERROR_THRESHOLD} lines with no PC
final String badTestFile = "tid=1,r1=0x1234\n".repeat(TenetLoader.ERROR_THRESHOLD + 1);
assertThrows(LoadException.class, () -> createTraceWithLoader(badTestFile));
}
/*
* Test how the loader handles overlapping memory reads/writes
*/
@Test
public void testMemoryValueOverlap() throws Exception {
final String testFile = """
tid=1,pc=0x1,r0=0x1234,mr=0x1000:0000000000001337
tid=2,pc=0x2,r1=0x4321,mw=0x2000:deadbeefdeadbeef
tid=1,pc=0x3,mw=0x1000:87654321,mr=0x2000:c0ffee
tid=1,pc=0x4,r1=0x4321
tid=1,pc=0x5,r1=0x4321
tid=1,pc=0x6,r1=0x4321
tid=1,pc=0x7,r1=0x4321
tid=1,pc=0x8,r1=0x4321
tid=1,pc=0x9,r1=0x4321
tid=1,pc=0xa,r1=0x4321
""";
final Trace trace = createTraceWithLoader(testFile);
final AddressSpace addrSpace =
trace.getBaseLanguage().getAddressFactory().getDefaultAddressSpace();
final ByteBuffer buffer =
ByteBuffer.allocate(trace.getBaseLanguage().getProgramCounter().getNumBytes());
trace.getMemoryManager().getBytes(0, addrSpace.getAddress(0x1000), buffer);
assertArrayEquals(this.arr(0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x13, 0x37), buffer.array());
buffer.clear();
trace.getMemoryManager().getBytes(1, addrSpace.getAddress(0x2000), buffer);
assertArrayEquals(this.arr(0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef), buffer.array());
buffer.clear();
trace.getMemoryManager().getBytes(2, addrSpace.getAddress(0x1000), buffer);
assertArrayEquals(this.arr(0x87, 0x65, 0x43, 0x21, 0x00, 0x00, 0x13, 0x37), buffer.array());
buffer.clear();
trace.getMemoryManager().getBytes(2, addrSpace.getAddress(0x2000), buffer);
assertArrayEquals(this.arr(0xc0, 0xff, 0xee, 0xef, 0xde, 0xad, 0xbe, 0xef), buffer.array());
}
/*
* Test that the loader continues after a line with no PC
*/
@Test
public void testNoPc() throws Exception {
final String testFile = """
tid=1,pc=0x1,r1=0x1234
tid=1,r2=0x1234
tid=1,pc=0x3,r1=0x4321
tid=1,pc=0x4,r1=0x4321
tid=1,pc=0x5,r1=0x4321
tid=1,pc=0x6,r1=0x4321
tid=1,pc=0x7,r1=0x4321
tid=1,pc=0x8,r1=0x4321
tid=1,pc=0x9,r1=0x4321
tid=1,pc=0xa,r1=0x4321
""";
final Trace trace = createTraceWithLoader(testFile);
assertNotNull(trace);
}
/*
* Test that the loader parses register values properly
*/
@Test
public void testRegisterValues() throws Exception {
final String testFile = """
tid=1,pc=0x1,r0=0x1234
tid=1,pc=0x2,r1=0x4321
tid=1,pc=0x3,r1=0x4321
tid=1,pc=0x4,r1=0x4321
tid=1,pc=0x5,r1=0x4321
tid=1,pc=0x6,r1=0x4321
tid=1,pc=0x7,r1=0x4321
tid=1,pc=0x8,r1=0x4321
tid=1,pc=0x9,r1=0x4321
tid=1,pc=0xa,r1=0x4321
""";
final Trace trace = createTraceWithLoader(testFile);
final TraceThread thread = Unique.assertOne(trace.getThreadManager().getAllThreads());
final TraceMemorySpace regs =
trace.getMemoryManager().getMemoryRegisterSpace(thread, false);
assertNotNull(regs);
assertEquals("1",
regs.getValue(0, program.getLanguage().getProgramCounter())
.getUnsignedValue()
.toString(16));
assertEquals("2",
regs.getValue(1, program.getLanguage().getProgramCounter())
.getUnsignedValue()
.toString(16));
}
/*
* Test that the loader parses register values properly
*/
@Test
public void testMultipleThreads() throws Exception {
final String testFile = """
tid=1,pc=0x1
tid=2,pc=0x2
tid=3,pc=0x3
""";
final Trace trace = createTraceWithLoader(testFile);
assertEquals(3, trace.getThreadManager().getAllThreads().size());
TraceThread thread =
trace.getThreadManager().getThreadsByPath("Threads[1]").stream().findFirst().get();
TraceMemorySpace t1_regs = trace.getMemoryManager().getMemoryRegisterSpace(thread, false);
assertNotNull(t1_regs);
assertEquals("1",
t1_regs.getValue(0, program.getLanguage().getProgramCounter())
.getUnsignedValue()
.toString(16));
thread = trace.getThreadManager().getThreadsByPath("Threads[2]").stream().findFirst().get();
TraceMemorySpace t2_regs = trace.getMemoryManager().getMemoryRegisterSpace(thread, false);
assertNotNull(t2_regs);
assertEquals("2",
t2_regs.getValue(1, program.getLanguage().getProgramCounter())
.getUnsignedValue()
.toString(16));
assertEquals("1",
t1_regs.getValue(1, program.getLanguage().getProgramCounter())
.getUnsignedValue()
.toString(16));
thread = trace.getThreadManager().getThreadsByPath("Threads[3]").stream().findFirst().get();
TraceMemorySpace t3_regs = trace.getMemoryManager().getMemoryRegisterSpace(thread, false);
assertNotNull(t3_regs);
assertEquals("3",
t3_regs.getValue(2, program.getLanguage().getProgramCounter())
.getUnsignedValue()
.toString(16));
assertEquals("2",
t2_regs.getValue(2, program.getLanguage().getProgramCounter())
.getUnsignedValue()
.toString(16));
assertEquals("1",
t1_regs.getValue(2, program.getLanguage().getProgramCounter())
.getUnsignedValue()
.toString(16));
}
}

View File

@@ -0,0 +1,376 @@
/* ###
* 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.breakpoint.timeline;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.*;
import javax.swing.JComponent;
import javax.swing.JScrollPane;
import docking.*;
import docking.action.DockingAction;
import docking.action.MenuData;
import docking.widgets.table.AnyObjectTableModel;
import docking.widgets.table.GTable;
import ghidra.app.plugin.core.byteviewer.ByteViewerActionContext;
import ghidra.app.plugin.core.codebrowser.CodeViewerActionContext;
import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingActionContext;
import ghidra.app.plugin.core.debug.gui.register.DebuggerRegisterActionContext;
import ghidra.app.services.DebuggerStaticMappingService;
import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.*;
import ghidra.program.util.ProgramLocation;
import ghidra.trace.model.Lifespan;
import ghidra.trace.model.Trace;
import ghidra.trace.model.stack.TraceStackFrame;
import ghidra.trace.model.symbol.TraceReference;
import ghidra.trace.model.target.TraceObjectValue;
import ghidra.util.Msg;
public class BreakpointTimelineActions {
protected record AddressSnap(Address address, Long snap) {}
private class BreakpointTimelineActionProvider extends ComponentProvider {
private final JComponent component;
private final List<AddressSnap> snaps;
public BreakpointTimelineActionProvider(Tool tool, String title,
List<AddressSnap> snapList) {
super(tool, title, "BreakpointTimelineActions");
snaps = new ArrayList<>(snapList);
final AnyObjectTableModel<AddressSnap> model =
new AnyObjectTableModel<>("AddressSnap", AddressSnap.class, "snap", "address");
model.setModelData(snaps);
final GTable table = new GTable(model);
table.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
final int row = table.getSelectedRow();
if (row != -1) {
tool.getService(DebuggerTraceManagerService.class)
.activateSnap(snaps.get(row).snap());
}
}
}
});
component = new JScrollPane(table);
setTransient();
setVisible(true);
}
@Override
public JComponent getComponent() {
return component;
}
}
private enum BreakType {
EXECUTE("Execution"), READ("Memory Read"), WRITE("Memory Write"), ACCESS("Memory Access");
String menuName;
BreakType(String menuName) {
this.menuName = menuName;
}
String plural() {
return switch (this) {
case EXECUTE -> "executions";
case READ -> "memory reads";
case WRITE -> "memory writes";
case ACCESS -> "memory accesses";
};
}
}
private enum SearchType {
FIRST("First..."),
PREVIOUS("Previous..."),
NEXT("Next..."),
FINAL("Final..."),
ALL("Show All...");
String menuGroup;
SearchType(String menuGroup) {
this.menuGroup = menuGroup;
}
}
private class TimelineAction extends DockingAction {
private final BreakType breakType;
private final SearchType searchType;
private final PluginTool tool;
private TimelineAction(PluginTool tool, SearchType searchType, BreakType breakType) {
super(breakType.menuName, "BreakpointTimelineHitAction");
this.breakType = breakType;
this.tool = tool;
this.searchType = searchType;
if (searchType != SearchType.ALL) {
setPopupMenuData(new MenuData(
new String[] { "Go to...", searchType.menuGroup, breakType.menuName }, null,
"BreakpointTimelineActions"));
}
else {
setPopupMenuData(
new MenuData(new String[] { searchType.menuGroup, breakType.menuName }, null,
"BreakpointTimelineActions"));
}
}
@Override
public void actionPerformed(ActionContext context) {
final AddressSet addrRange = getAddessRangeFromContext(context);
final Lifespan lifespan = getLifespan();
final Trace currentTrace =
tool.getService(DebuggerTraceManagerService.class).getCurrentTrace();
switch (breakType) {
case EXECUTE -> handleExecutionAction(addrRange, lifespan, currentTrace);
case READ, WRITE, ACCESS -> handleMemoryAction(addrRange, lifespan, currentTrace);
}
}
private AddressSet getAddessRangeFromContext(ActionContext context) {
return switch (context) {
case final CodeViewerActionContext c -> getAddressRangeFromCodeViewerActionContext(
c);
case final DebuggerListingActionContext c -> getAddressRangeFromDebuggerListingActionContext(
c);
case final DebuggerRegisterActionContext c -> getAddressRangeFromDebuggerRegisterActionContext(
c);
case final ByteViewerActionContext c -> getAddressRangeFromByteViewerActionContext(
c);
default -> null;
};
}
private AddressSet getAddressRangeFromByteViewerActionContext(ByteViewerActionContext c) {
final AddressSet dynamicSet = new AddressSet();
for (final AddressRange range : c.getSelection().getAddressRanges()) {
dynamicSet.add(range.getMinAddress(), range.getMaxAddress());
}
return dynamicSet;
}
private AddressSet getAddressRangeFromCodeViewerActionContext(CodeViewerActionContext c) {
final Trace trace =
tool.getService(DebuggerTraceManagerService.class).getCurrentTrace();
final DebuggerStaticMappingService staticMappingService =
tool.getService(DebuggerStaticMappingService.class);
final AddressSet dynamicSet = new AddressSet();
if (c.getSelection().isEmpty()) {
final ProgramLocation dynamicLocation =
staticMappingService.getDynamicLocationFromStatic(trace.getProgramView(),
new ProgramLocation(c.getProgram(), c.getAddress()));
if (dynamicLocation == null) {
Msg.showInfo(null, null, "No mapping", "No mapping for %s @ 0x%x"
.formatted(c.getProgram(), c.getAddress().getOffset()));
}
else {
dynamicSet.add(dynamicLocation.getAddress());
}
}
else {
for (final AddressRange range : c.getSelection().getAddressRanges()) {
final ProgramLocation startDynamicLocation =
staticMappingService.getDynamicLocationFromStatic(trace.getProgramView(),
new ProgramLocation(c.getProgram(), range.getMinAddress()));
if (startDynamicLocation == null) {
Msg.showInfo(null, null, "No mapping", "No mapping for %s @ 0x%x"
.formatted(c.getProgram(), range.getMinAddress().getOffset()));
continue;
}
final ProgramLocation endDynamicLocation =
staticMappingService.getDynamicLocationFromStatic(trace.getProgramView(),
new ProgramLocation(c.getProgram(), range.getMaxAddress()));
if (endDynamicLocation == null) {
Msg.showInfo(null, null, "No mapping", "No mapping for %s @ 0x%x"
.formatted(c.getProgram(), range.getMaxAddress().getOffset()));
continue;
}
dynamicSet.add(startDynamicLocation.getAddress(),
endDynamicLocation.getAddress());
}
}
return dynamicSet;
}
private AddressSet getAddressRangeFromDebuggerListingActionContext(
DebuggerListingActionContext c) {
final AddressSet dynamicSet = new AddressSet();
if (c.getSelection().isEmpty()) {
dynamicSet.add(c.getAddress());
}
else {
for (final AddressRange range : c.getSelection().getAddressRanges()) {
dynamicSet.add(range.getMinAddress(), range.getMaxAddress());
}
}
return dynamicSet;
}
private AddressSet getAddressRangeFromDebuggerRegisterActionContext(
DebuggerRegisterActionContext c) {
final AddressSpace addressSpace = tool.getService(DebuggerTraceManagerService.class)
.getCurrentTrace()
.getProgramView()
.getAddressFactory()
.getDefaultAddressSpace();
final Long regValue = c.getSelected().getValue().longValue();
final Address addr = addressSpace.getAddress(regValue);
final AddressSet addressSet = new AddressSet();
addressSet.add(addr);
return addressSet;
}
private Lifespan getLifespan() {
final long currentSnap =
tool.getService(DebuggerTraceManagerService.class).getCurrentSnap();
return switch (searchType) {
case ALL, FINAL, FIRST -> Lifespan.ALL;
case NEXT -> Lifespan.nowOn(currentSnap + 1);
case PREVIOUS -> Lifespan.before(currentSnap);
};
}
private void handleExecutionAction(AddressSet addrSet, Lifespan lifespan,
Trace currentTrace) {
final List<? extends TraceObjectValue> hits = currentTrace.getObjectManager()
.getRootSchema()
.getContext()
.getAllSchemas()
.stream()
.filter(e -> e.getInterfaces().contains(TraceStackFrame.class))
.map(s -> s.checkAliasedAttribute(TraceStackFrame.KEY_PC))
.flatMap(e -> addrSet.stream()
.flatMap(addrRange -> currentTrace.getObjectManager()
.getValuesIntersecting(lifespan, addrRange, e)
.stream()))
.sorted(Comparator.comparingLong(TraceObjectValue::getMinSnap))
.toList();
if (hits.size() > 0) {
if (searchType == SearchType.ALL) {
new BreakpointTimelineActionProvider(tool,
"All executions in %s - %s".formatted(addrSet.getMinAddress(),
addrSet.getMaxAddress()),
hits.stream()
.map(c -> new AddressSnap(c.castValue(), c.getMinSnap()))
.toList());
return;
}
final Long snap = switch (searchType) {
case FINAL, PREVIOUS -> hits.getLast().getMinSnap();
case FIRST, NEXT -> hits.getFirst().getMinSnap();
default -> null;
};
if (snap == null) {
return;
}
tool.getService(DebuggerTraceManagerService.class).activateSnap(snap);
}
else if (searchType == SearchType.ALL) {
Msg.showInfo(this, null, "%s search".formatted(breakType.name()),
"There are no executions at this location".formatted(breakType.name()));
}
else {
Msg.showInfo(this, null, "%s search".formatted(breakType.name()),
"There is no %s execution at this location"
.formatted(searchType.name().toLowerCase()));
}
}
private void handleMemoryAction(AddressSet addrSet, Lifespan lifespan, Trace currentTrace) {
final List<? extends TraceReference> hits = addrSet.stream()
.flatMap(addrRange -> currentTrace.getReferenceManager()
.getReferencesToRange(lifespan, addrRange)
.stream())
.sorted(Comparator.comparingLong(TraceReference::getStartSnap))
.toList();
if (hits.size() > 0) {
if (searchType == SearchType.ALL) {
new BreakpointTimelineActionProvider(tool,
"All %s in %s - %s".formatted(breakType.name(), addrSet.getMinAddress(),
addrSet.getMaxAddress()),
hits.stream()
.map(c -> new AddressSnap(c.getFromAddress(), c.getStartSnap()))
.toList());
return;
}
final Long snap = switch (searchType) {
case FINAL, PREVIOUS -> hits.getLast().getStartSnap();
case FIRST, NEXT -> hits.getFirst().getStartSnap();
default -> null;
};
if (snap == null) {
return;
}
tool.getService(DebuggerTraceManagerService.class).activateSnap(snap);
}
else if (searchType == SearchType.ALL) {
Msg.showInfo(this, null, "%s search".formatted(breakType.name()),
"There are no %s to this location".formatted(breakType.plural()));
}
else {
Msg.showInfo(this, null, "%s search".formatted(breakType.name()),
"There is no %s %s to this location".formatted(searchType.name().toLowerCase(),
breakType.menuName.toLowerCase()));
}
}
@Override
public boolean isEnabledForContext(ActionContext context) {
return ((context instanceof CodeViewerActionContext) ||
(context instanceof DebuggerListingActionContext) ||
(context instanceof DebuggerRegisterActionContext) ||
(context instanceof ByteViewerActionContext));
}
}
private final PluginTool tool;
List<TimelineAction> actions;
BreakpointTimelineActions(PluginTool tool) {
this.tool = tool;
actions = new ArrayList<>();
for (final SearchType searchType : SearchType.values()) {
for (final BreakType breakType : BreakType.values()) {
final TimelineAction action = new TimelineAction(tool, searchType, breakType);
tool.addAction(action);
}
}
}
void dispose() {
for (final TimelineAction action : actions) {
tool.removeAction(action);
}
}
}

View File

@@ -180,28 +180,7 @@ class BreakpointTimelinePanel extends JPanel {
}
if (!BreakpointTimelinePanel.singleColumn) {
final long numCells = visibleEnd - visibleStart;
double xSideLength;
double ySideLength;
final double xPixelsPerCell = Math.ceil(Math.sqrt((numCells * width) / height));
if ((Math.floor((xPixelsPerCell * height) / width) * xPixelsPerCell) < numCells) {
xSideLength = (height / Math.ceil((height * xPixelsPerCell) / width));
}
else {
xSideLength = width / xPixelsPerCell;
}
final double yPixelsPerCell = Math.ceil(Math.sqrt((numCells * height) / width));
if ((Math.floor((yPixelsPerCell * width) / height) * yPixelsPerCell) < numCells) {
ySideLength = (width / Math.ceil((width * yPixelsPerCell) / height));
}
else {
ySideLength = height / yPixelsPerCell;
}
final long potentialSideLength = (long) Math.max(xSideLength, ySideLength);
final long potentialSideLength = getPotentialSideLength(width, height);
cellWidth = Math.max(defaultCellSize, potentialSideLength);
cellHeight = cellWidth;
@@ -250,6 +229,34 @@ class BreakpointTimelinePanel extends JPanel {
repaint();
}
private long getPotentialSideLength(int width, int height) {
final long numCells = visibleEnd - visibleStart;
double xSideLength;
double ySideLength;
double xPixelsPerCell = Math.ceil(Math.sqrt((numCells * width) / height));
if (xPixelsPerCell == 0) {
xPixelsPerCell = width;
}
if ((Math.floor((xPixelsPerCell * height) / width) * xPixelsPerCell) < numCells) {
xSideLength = (height / Math.ceil((height * xPixelsPerCell) / width));
}
else {
xSideLength = width / xPixelsPerCell;
}
final double yPixelsPerCell = Math.ceil(Math.sqrt((numCells * height) / width));
if ((Math.floor((yPixelsPerCell * width) / height) * yPixelsPerCell) < numCells) {
ySideLength = (width / Math.ceil((width * yPixelsPerCell) / height));
}
else {
ySideLength = height / yPixelsPerCell;
}
return (long) Math.max(xSideLength, ySideLength);
}
@Override
public String getToolTipText(MouseEvent event) {
final Optional<BreakpointTimelinePanel.CachedIndex> first =

View File

@@ -22,6 +22,7 @@ import ghidra.app.plugin.core.debug.AbstractDebuggerPlugin;
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.services.DebuggerStaticMappingService;
import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
@@ -33,11 +34,13 @@ import ghidra.trace.model.Trace;
category = PluginCategoryNames.DEBUGGER,
packageName = DebuggerPluginPackage.NAME,
status = PluginStatus.UNSTABLE,
servicesRequired = { DebuggerTraceManagerService.class, },
servicesRequired = { DebuggerTraceManagerService.class, DebuggerStaticMappingService.class },
eventsConsumed = { TraceClosedPluginEvent.class, TraceActivatedPluginEvent.class, }
)
public class BreakpointTimelinePlugin extends AbstractDebuggerPlugin {
BreakpointTimelineProvider provider;
BreakpointTimelineActions actions;
private Trace currentTrace;
private final Map<Trace, List<BreakpointTimelineProvider>> traceSpecificZoomProviders =
@@ -54,6 +57,7 @@ public class BreakpointTimelinePlugin extends AbstractDebuggerPlugin {
@Override
protected void dispose() {
actions.dispose();
for (final var providers : traceSpecificZoomProviders.values()) {
for (final var provider : providers) {
tool.removeComponentProvider(provider);
@@ -76,6 +80,7 @@ public class BreakpointTimelinePlugin extends AbstractDebuggerPlugin {
protected void init() {
super.init();
provider = new BreakpointTimelineProvider(this);
actions = new BreakpointTimelineActions(getTool());
}
@Override

View File

@@ -26,6 +26,8 @@ import docking.ComponentProvider;
import docking.action.DockingAction;
import docking.action.ToolBarData;
import generic.theme.GIcon;
import ghidra.framework.model.DomainObjectEvent;
import ghidra.lifecycle.Internal;
import ghidra.program.model.address.AddressRange;
import ghidra.program.model.symbol.RefType;
import ghidra.trace.model.*;
@@ -36,7 +38,7 @@ import ghidra.trace.model.symbol.TraceReference;
import ghidra.trace.model.target.TraceObjectValue;
import ghidra.trace.util.TraceEvents;
class BreakpointTimelineProvider extends ComponentProvider {
public class BreakpointTimelineProvider extends ComponentProvider {
record BreakpointHitEvent(long snap, TraceBreakpointKind breakType, String breakpointName) {}
private class BreakpointTimeOverviewEventListener extends TraceDomainObjectListener {
@@ -44,6 +46,10 @@ class BreakpointTimelineProvider extends ComponentProvider {
public BreakpointTimeOverviewEventListener() {
listenFor(TraceEvents.BREAKPOINT_CHANGED, this::breakpointChanged);
listenFor(TraceEvents.BREAKPOINT_DELETED, this::breakpointDeleted);
listenFor(TraceEvents.SNAPSHOT_ADDED, this::snapshotEvent);
listenFor(TraceEvents.SNAPSHOT_DELETED, this::snapshotEvent);
listenFor(TraceEvents.SNAPSHOT_CHANGED, this::snapshotEvent);
listenForUntyped(DomainObjectEvent.RESTORED, e -> snapshotEvent());
}
void breakpointChanged(TraceBreakpointLocation tb) {
@@ -56,10 +62,21 @@ class BreakpointTimelineProvider extends ComponentProvider {
breakpointTimelinePlugin.refreshAllProviders(null);
}
void snapshotEvent() {
final long newMaxSnap =
Objects.requireNonNullElse(currentTrace.getTimeManager().getMaxSnap(), 0)
.longValue();
if (newMaxSnap != curMaxSnap) {
breakpointTimelinePanel.setEventsAndVisibleRange(breakpointHits, 0, newMaxSnap);
curMaxSnap = newMaxSnap;
}
}
}
private class CloseAllZoomWindowsAction extends DockingAction {
private final GIcon ICON = new GIcon("icon.debugger.breakpoint.timeline.close_all_zoom_windows");
private final GIcon ICON =
new GIcon("icon.debugger.breakpoint.timeline.close_all_zoom_windows");
CloseAllZoomWindowsAction(ComponentProvider provider) {
super("Close all zoom windows", provider.getOwner());
@@ -90,7 +107,8 @@ class BreakpointTimelineProvider extends ComponentProvider {
private class ToggleGridAction extends DockingAction {
private final GIcon OUTLINE_ICON = new GIcon("icon.debugger.breakpoint.timeline.outline");
private final GIcon NO_OUTLINE_ICON = new GIcon("icon.debugger.breakpoint.timeline.no_outline");
private final GIcon NO_OUTLINE_ICON =
new GIcon("icon.debugger.breakpoint.timeline.no_outline");
private boolean grid = true;
ToggleGridAction(ComponentProvider provider) {
@@ -160,13 +178,30 @@ class BreakpointTimelineProvider extends ComponentProvider {
}
}
private Trace currentTrace;
private final BreakpointTimelinePanel breakpointTimelinePanel;
@Internal
public static Iterator<? extends TraceObjectValue> getTraceObjectValuesWithPCsIntersectingRange(
Trace trace, AddressRange range) {
return trace.getObjectManager()
.getRootSchema()
.getContext()
.getAllSchemas()
.stream()
.filter(s -> s.getInterfaces().contains(TraceStackFrame.class))
.map(s -> s.checkAliasedAttribute(TraceStackFrame.KEY_PC))
.flatMap(e -> trace.getObjectManager()
.getValuesIntersecting(Lifespan.ALL, range, e)
.stream())
.sorted(Comparator.comparingLong(TraceObjectValue::getMinSnap))
.iterator();
}
private final BreakpointTimelinePanel breakpointTimelinePanel;
private final JPanel wrapperPanel;
private final BreakpointTimelinePlugin breakpointTimelinePlugin;
private final BreakpointTimeOverviewEventListener listener =
new BreakpointTimeOverviewEventListener();
private Trace currentTrace;
private long curMaxSnap;
private List<BreakpointHitEvent> breakpointHits;
BreakpointTimelineProvider(BreakpointTimelinePlugin breakpointTimelinePlugin) {
@@ -217,7 +252,8 @@ class BreakpointTimelineProvider extends ComponentProvider {
private void findAndAddAllBreakpointHitsAtLocation(TraceBreakpointLocation breakpointLocation,
AddressRange range, String kind) {
for (final TraceBreakpointKind breakpointKind : TraceBreakpointKindSet.decode(kind, false)) {
for (final TraceBreakpointKind breakpointKind : TraceBreakpointKindSet.decode(kind,
false)) {
switch (breakpointKind) {
case HW_EXECUTE, SW_EXECUTE -> findAndAddExecuteBreakpointHits(breakpointLocation,
range);
@@ -229,9 +265,11 @@ class BreakpointTimelineProvider extends ComponentProvider {
private void findAndAddExecuteBreakpointHits(TraceBreakpointLocation breakpointLocation,
AddressRange range) {
final Collection<? extends TraceObjectValue> intersecting = currentTrace.getObjectManager()
.getValuesIntersecting(Lifespan.ALL, range, TraceStackFrame.KEY_PC);
for (final TraceObjectValue tov : intersecting) {
final Iterator<? extends TraceObjectValue> intersecting =
getTraceObjectValuesWithPCsIntersectingRange(currentTrace, range);
while (intersecting.hasNext()) {
final TraceObjectValue tov = intersecting.next();
breakpointHits.add(new BreakpointHitEvent(tov.getMinSnap(),
TraceBreakpointKind.SW_EXECUTE, breakpointLocation.getName(tov.getMinSnap())));
}
@@ -302,15 +340,17 @@ class BreakpointTimelineProvider extends ComponentProvider {
currentTrace.removeListener(listener);
}
currentTrace = trace;
curMaxSnap = 0;
refreshBreakpointHits();
if (currentTrace != null) {
breakpointTimelinePanel.setEventsAndVisibleRange(breakpointHits, 0,
currentTrace.getTimeManager().getMaxSnap());
curMaxSnap = Objects.requireNonNullElse(currentTrace.getTimeManager().getMaxSnap(), 0)
.longValue();
breakpointTimelinePanel.setEventsAndVisibleRange(breakpointHits, 0, curMaxSnap);
currentTrace.addListener(listener);
}
else {
breakpointTimelinePanel.setEventsAndVisibleRange(null, 0, 0);
}
}
}
}

View File

@@ -17,10 +17,14 @@ package ghidra.app.plugin.core.debug.gui.listing;
import static ghidra.app.plugin.core.debug.gui.DebuggerResources.GROUP_TRANSIENT_VIEWS;
import java.util.ArrayList;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import docking.ComponentProvider;
import ghidra.debug.api.listing.DebuggerListing;
import org.jdom2.Element;
import docking.ActionContext;
@@ -39,6 +43,7 @@ import ghidra.app.util.viewer.format.FormatManager;
import ghidra.app.util.viewer.listingpanel.ListingPanel;
import ghidra.debug.api.action.AutoReadMemorySpec;
import ghidra.debug.api.action.LocationTrackingSpec;
import ghidra.debug.api.listing.DebuggerListing;
import ghidra.debug.api.listing.MultiBlendedListingBackgroundColorModel;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.options.SaveState;
@@ -180,6 +185,22 @@ public class DebuggerListingPlugin extends AbstractCodeBrowserPlugin<DebuggerLis
}
}
@Override
public DebuggerListing createNewListing() {
return createNewDisconnectedProvider();
}
public String findNextCustomTitle() {
List<String> allTitles = disconnectedProviders.stream().map(ComponentProvider::getTitle).toList();
int i;
for (i = 0; i < allTitles.size(); i++) {
if (!allTitles.contains("Dynamic %d".formatted(i))) {
return "Dynamic %d".formatted(i);
}
}
return "Dynamic %d".formatted(i);
}
@Override
protected void setView(AddressSetView addrSet) {
TraceProgramView view = current.getView();
@@ -352,6 +373,14 @@ public class DebuggerListingPlugin extends AbstractCodeBrowserPlugin<DebuggerLis
return goTo(loc, centerOnScreen);
}
@Override
public List<DebuggerListing> getAllListings() {
final List<DebuggerListing> ret = new ArrayList<>();
ret.add(connectedProvider);
ret.addAll(disconnectedProviders);
return ret;
}
@Override
public void writeDataState(SaveState saveState) {
SaveState connectedProviderState = new SaveState();

View File

@@ -67,6 +67,7 @@ import ghidra.async.AsyncDebouncer;
import ghidra.async.AsyncTimer;
import ghidra.debug.api.action.*;
import ghidra.debug.api.control.ControlMode;
import ghidra.debug.api.listing.DebuggerListing;
import ghidra.debug.api.listing.MultiBlendedListingBackgroundColorModel;
import ghidra.debug.api.modules.DebuggerStaticMappingChangeListener;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
@@ -90,7 +91,7 @@ import ghidra.util.datastruct.ListenerSet;
import utilities.util.SuppressableCallback;
import utilities.util.SuppressableCallback.Suppression;
public class DebuggerListingProvider extends CodeViewerProvider {
public class DebuggerListingProvider extends CodeViewerProvider implements DebuggerListing {
private static final AutoConfigState.ClassHandler<
DebuggerListingProvider> CONFIG_STATE_HANDLER =
@@ -500,6 +501,9 @@ public class DebuggerListingProvider extends CodeViewerProvider {
CONFIG_STATE_HANDLER.writeConfigState(this, saveState);
trackingTrait.writeConfigState(saveState);
readsMemTrait.writeConfigState(saveState);
if (!isMainListing() && getTitle() != null) {
saveState.putString("title", getTitle());
}
}
void readConfigState(SaveState saveState) {
@@ -514,7 +518,9 @@ public class DebuggerListingProvider extends CodeViewerProvider {
CONFIG_STATE_HANDLER.readConfigState(this, saveState);
trackingTrait.readConfigState(saveState);
readsMemTrait.readConfigState(saveState);
if (!isMainListing() && saveState.hasValue("title")) {
setTitle(saveState.getString("title", ""));
}
if (isMainListing()) {
followsCurrentThread = true;
}

View File

@@ -884,7 +884,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter
if (sel == null || sel.size() != 1) {
return;
}
mapModuleTo(sel.iterator().next());
mapModuleTo(sel.iterator().next(), current.getSnap());
}
private void activatedMapSections(ActionContext context) {
@@ -940,7 +940,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter
}
private void activatedMapMissingModule(DebuggerMissingModuleActionContext context) {
mapModuleTo(context.getModule());
mapModuleTo(context.getModule(), context.getSnap());
}
private void activatedMapMissingProgramRetry(DebuggerMissingProgramActionContext context) {
@@ -1149,7 +1149,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter
promptModuleProposal(proposal, NO_MODULES_PROPOSAL_SEL);
}
protected void mapModuleTo(TraceModule module) {
protected void mapModuleTo(TraceModule module, long snap) {
if (staticMappingService == null) {
return;
}
@@ -1157,8 +1157,9 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter
if (program == null) {
return;
}
long curLifeStart = module.getObject().getLife().spanContaining(snap).min();
ModuleMapProposal proposal =
staticMappingService.proposeModuleMap(module, current.getSnap(), program);
staticMappingService.proposeModuleMap(module, curLifeStart, program);
Map<TraceModule, ModuleMapEntry> map = proposal.computeMap();
promptModuleProposal(map.values(), NO_MODULES_PROPOSAL_SEL);
}

View File

@@ -28,6 +28,7 @@ import docking.action.DockingActionIf;
import docking.action.builder.ActionBuilder;
import generic.ULongSpan;
import generic.theme.GThemeDefaults.Colors;
import ghidra.app.plugin.core.debug.gui.breakpoint.timeline.BreakpointTimelineProvider;
import ghidra.app.plugin.core.debug.gui.timeoverview.*;
import ghidra.app.plugin.core.overview.OverviewColorLegendDialog;
import ghidra.app.services.DebuggerTraceManagerService;
@@ -38,7 +39,6 @@ import ghidra.program.model.symbol.RefType;
import ghidra.trace.model.*;
import ghidra.trace.model.breakpoint.*;
import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet;
import ghidra.trace.model.stack.TraceStackFrame;
import ghidra.trace.model.symbol.TraceReference;
import ghidra.trace.model.target.TraceObjectValue;
import ghidra.trace.util.TraceEvents;
@@ -86,27 +86,21 @@ public class BreakpointTimeOverviewColorService implements TimeOverviewColorServ
}
private static String OPTIONS_NAME = "Breakpoint Hit Timeline";
private PluginTool tool;
Trace currentTrace;
TimeOverviewColorComponent overviewComponent;
DialogComponentProvider legendDialog;
BreakTypeOverviewLegendPanel legendPanel;
TimeOverviewColorPlugin plugin;
private final BreakpointTimeOverviewEventListener eventListener =
new BreakpointTimeOverviewEventListener();
private final Map<CellType, Color> colorMap = new HashMap<>();
private final Map<Integer, Long> indexToSnap = new HashMap<>();
private final Map<Long, Color> snapToColor = new HashMap<>();
private final Map<Long, String> snapToTooltip = new HashMap<>();
private final Map<Long, ULongSpan> snapToRange = new HashMap<>();
Trace currentTrace;
TimeOverviewColorComponent overviewComponent;
DialogComponentProvider legendDialog;
BreakTypeOverviewLegendPanel legendPanel;
TimeOverviewColorPlugin plugin;
List<BreakpointEvent> snapsWithBreakpointsHit = new ArrayList<>();
Lifespan bounds;
private PluginTool tool;
private DebuggerTraceManagerService debuggerTraceManagerService;
protected void calculateBreakpointHits() {
@@ -192,9 +186,11 @@ public class BreakpointTimeOverviewColorService implements TimeOverviewColorServ
private void findAndAddExecuteBreakpointHits(TraceBreakpointLocation breakpointLocation,
AddressRange range) {
final Collection<? extends TraceObjectValue> intersecting = currentTrace.getObjectManager()
.getValuesIntersecting(Lifespan.ALL, range, TraceStackFrame.KEY_PC);
for (final TraceObjectValue tov : intersecting) {
final Iterator<? extends TraceObjectValue> intersecting = BreakpointTimelineProvider
.getTraceObjectValuesWithPCsIntersectingRange(currentTrace, range);
while (intersecting.hasNext()) {
final TraceObjectValue tov = intersecting.next();
snapsWithBreakpointsHit
.add(new BreakpointEvent(tov.getMinSnap(), CellType.INSTRUCTION_EXECUTED));
}
@@ -240,7 +236,8 @@ public class BreakpointTimeOverviewColorService implements TimeOverviewColorServ
/**
* Returns the color associated with the given {@link CellType}
*
* @param breakType the span type for which to get a color.
* @param breakType
* the span type for which to get a color.
* @return the color associated with the given {@link CellType}
*/
public Color getColor(CellType breakType) {
@@ -358,4 +355,4 @@ public class BreakpointTimeOverviewColorService implements TimeOverviewColorServ
bounds = Lifespan.span(0, getTrace().getTimeManager().getMaxSnap());
}
}
}
}

View File

@@ -0,0 +1,101 @@
/* ###
* 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.tracecalltree;
import java.util.List;
import docking.widgets.gtreetable.GTreeTableNode;
import ghidra.trace.model.time.TraceSnapshot;
import ghidra.util.NumericUtilities;
import resources.Icons;
abstract class AbstractTraceCallTreeNode extends GTreeTableNode {
public record ParamNameToBytes(String name, byte[] bytes) {}
private final TraceSnapshot snap;
private final List<ParamNameToBytes> parameters;
private final byte[] returnVal;
private final String module;
private int largestParamSize = 0;
public AbstractTraceCallTreeNode(final String name, final String module,
final TraceSnapshot snap, List<ParamNameToBytes> parameters, final byte[] returnVal) {
super(name);
icon = Icons.RIGHT_ICON;
this.snap = snap;
this.parameters = parameters;
this.returnVal = returnVal;
this.module = module;
}
public int getLargestParamSize() {
return largestParamSize;
}
public String getModule() {
return module;
}
public ParamNameToBytes getParameter(int i) {
if ((parameters != null) && (i >= 0) && (i < parameters.size())) {
return parameters.get(i);
}
return null;
}
public int getParameterNumber() {
return parameters.size();
}
public List<ParamNameToBytes> getParameters() {
return parameters;
}
public String getParameterString(int i) {
if ((parameters != null) && (i >= 0) && (i < parameters.size())) {
return "%s: %s".formatted(parameters.get(i).name,
NumericUtilities.convertBytesToString(parameters.get(i).bytes));
}
return "";
}
public byte[] getReturnVal() {
return returnVal;
}
public String getReturnValString() {
if (returnVal == null) {
return "";
}
return "Return: %s".formatted(NumericUtilities.convertBytesToString(returnVal));
}
public long getSnapshotKey() {
if (snap == null) {
return -1;
}
return snap.getKey();
}
@Override
public String getTreeData() {
return name;
}
public void setLargestParamSize(int largestParamSize) {
this.largestParamSize = largestParamSize;
}
}

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.tracecalltree;
import java.util.List;
import docking.DefaultActionContext;
import docking.widgets.table.GTable;
public class TraceCallTreeActionContext extends DefaultActionContext {
private final List<AbstractTraceCallTreeNode> selected;
public TraceCallTreeActionContext(TraceCallTreeProvider provider,
List<AbstractTraceCallTreeNode> selected, GTable source) {
super(provider, selected, source);
this.selected = selected;
}
public List<AbstractTraceCallTreeNode> getSelected() {
return selected;
}
}

View File

@@ -0,0 +1,27 @@
/* ###
* 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.tracecalltree;
import java.util.List;
import ghidra.trace.model.time.TraceSnapshot;
public class TraceCallTreeCallNode extends AbstractTraceCallTreeNode {
public TraceCallTreeCallNode(String name, String module, TraceSnapshot snap,
List<ParamNameToBytes> parameters, byte[] returnVal) {
super(name, module, snap, parameters, returnVal);
}
}

View File

@@ -0,0 +1,34 @@
/* ###
* 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.tracecalltree;
import java.util.List;
import ghidra.trace.model.time.TraceSnapshot;
import resources.Icons;
public class TraceCallTreeExternalNode extends AbstractTraceCallTreeNode {
public TraceCallTreeExternalNode(String name, String module, TraceSnapshot snap,
List<ParamNameToBytes> parameters, byte[] returnVal) {
super(name, module, snap, parameters, returnVal);
icon = Icons.STRONG_WARNING_ICON;
}
@Override
public String getTreeData() {
return "External: %s".formatted(name);
}
}

View File

@@ -0,0 +1,57 @@
/* ###
* 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.tracecalltree;
import java.util.Objects;
import docking.DefaultActionContext;
import ghidra.program.model.address.Address;
import ghidra.trace.model.Trace;
public class TraceCallTreeLogContext extends DefaultActionContext {
private final Trace trace;
private final Address dynamicPC;
private final int hashCode;
public TraceCallTreeLogContext(TraceCallTreeProvider provider, Trace trace, Address dynamicPC) {
super(provider);
this.trace = trace;
this.dynamicPC = dynamicPC;
hashCode = Objects.hash(getClass(), trace, dynamicPC);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof TraceCallTreeLogContext that)) {
return false;
}
if (!Objects.equals(this.trace, that.trace)) {
return false;
}
if (!Objects.equals(this.dynamicPC, that.dynamicPC)) {
return false;
}
return true;
}
@Override
public int hashCode() {
return hashCode;
}
}

View File

@@ -0,0 +1,163 @@
/* ###
* 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.tracecalltree;
import java.util.HashMap;
import java.util.Objects;
import docking.widgets.table.AbstractDynamicTableColumn;
import docking.widgets.table.TableColumnDescriptor;
import docking.widgets.table.threaded.ThreadedTableModelStub;
import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.program.model.address.Address;
import ghidra.trace.model.Trace;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
public class TraceCallTreeLogModel
extends ThreadedTableModelStub<TraceCallTreeLogModel.TraceCallTreeLogObject> {
private static class DynamicAddressColumn
extends AbstractDynamicTableColumn<TraceCallTreeLogObject, Address, Object> {
@Override
public String getColumnName() {
return "Dynamic Address";
}
@Override
public Address getValue(TraceCallTreeLogObject rowObject, Settings settings, Object data,
ServiceProvider services) throws IllegalArgumentException {
return rowObject.dynamicAddress();
}
}
private static class MessageColumn
extends AbstractDynamicTableColumn<TraceCallTreeLogObject, String, Object> {
@Override
public String getColumnName() {
return "Message";
}
@Override
public String getValue(TraceCallTreeLogObject rowObject, Settings settings, Object data,
ServiceProvider services) throws IllegalArgumentException {
return rowObject.log();
}
}
private static class SnapColumn
extends AbstractDynamicTableColumn<TraceCallTreeLogObject, Long, Object> {
@Override
public String getColumnName() {
return "Snap";
}
@Override
public Long getValue(TraceCallTreeLogObject rowObject, Settings settings, Object data,
ServiceProvider services) throws IllegalArgumentException {
return rowObject.snap();
}
}
private static class StaticAddressColumn
extends AbstractDynamicTableColumn<TraceCallTreeLogObject, Address, Object> {
@Override
public String getColumnName() {
return "Static Address";
}
@Override
public Address getValue(TraceCallTreeLogObject rowObject, Settings settings, Object data,
ServiceProvider services) throws IllegalArgumentException {
return rowObject.staticAddress();
}
}
public record TraceCallTreeLogObject(Trace trace, String log, long snap, Address dynamicAddress,
Address staticAddress) {
public TraceCallTreeLogObject {
Objects.requireNonNull(log);
}
}
private static class TraceColumn
extends AbstractDynamicTableColumn<TraceCallTreeLogObject, String, Object> {
@Override
public String getColumnName() {
return "Trace";
}
@Override
public String getValue(TraceCallTreeLogObject rowObject, Settings settings, Object data,
ServiceProvider services) throws IllegalArgumentException {
return rowObject.trace().getName();
}
}
record LogKey(String traceName, Address dynamicAddress, long snap) {}
HashMap<LogKey, TraceCallTreeLogObject> logs;
protected TraceCallTreeLogModel(TraceCallTreePlugin plugin) {
super("Trace Call Tree Log Model", plugin.getTool());
logs = new HashMap<>();
}
void clear() {
logs.clear();
reload();
}
@Override
protected TableColumnDescriptor<TraceCallTreeLogObject> createTableColumnDescriptor() {
final TableColumnDescriptor<TraceCallTreeLogObject> descriptor =
new TableColumnDescriptor<>();
descriptor.addVisibleColumn(new TraceColumn());
descriptor.addVisibleColumn(new SnapColumn());
descriptor.addVisibleColumn(new MessageColumn());
descriptor.addVisibleColumn(new DynamicAddressColumn());
descriptor.addVisibleColumn(new StaticAddressColumn());
return descriptor;
}
@Override
protected void doLoad(Accumulator<TraceCallTreeLogObject> accumulator, TaskMonitor monitor)
throws CancelledException {
accumulator.addAll(logs.values());
}
void log(Trace trace, String msg, long snap, Address staticAddress, Address dynamicAddress) {
final TraceCallTreeLogObject log =
new TraceCallTreeLogObject(trace, msg, snap, dynamicAddress, staticAddress);
final LogKey key = new LogKey(trace.getName(), dynamicAddress, snap);
logs.remove(key);
logs.put(key, log);
reload();
}
void resolve(Trace trace, long snap, Address dynamicAddress) {
final LogKey key = new LogKey(trace.getName(), dynamicAddress, snap);
logs.remove(key);
reload();
}
}

View File

@@ -0,0 +1,142 @@
/* ###
* 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.tracecalltree;
import java.util.HashSet;
import java.util.Set;
import docking.widgets.gtreetable.GTreeTableModel;
import docking.widgets.table.*;
import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.ServiceProvider;
class TraceCallTreeModel extends GTreeTableModel<AbstractTraceCallTreeNode> {
private static class ModuleColumn
extends AbstractDynamicTableColumnStub<AbstractTraceCallTreeNode, String> {
private final String columnName;
ModuleColumn(String columnName) {
this.columnName = columnName;
}
@Override
public String getColumnName() {
return columnName;
}
@Override
public String getValue(AbstractTraceCallTreeNode rowObject, Settings settings,
ServiceProvider provider) {
return rowObject.getModule();
}
}
private static class ParameterColumn
extends AbstractDynamicTableColumnStub<AbstractTraceCallTreeNode, String> {
private final int paramNum;
ParameterColumn(int paramNum) {
this.paramNum = paramNum;
}
@Override
public String getColumnName() {
return "Parameter %d".formatted(paramNum);
}
@Override
public String getValue(AbstractTraceCallTreeNode rowObject, Settings settings,
ServiceProvider provider) {
return rowObject.getParameterString(paramNum);
}
}
private static class ReturnColumn
extends AbstractDynamicTableColumnStub<AbstractTraceCallTreeNode, String> {
private final String columnName;
ReturnColumn(String columnName) {
this.columnName = columnName;
}
@Override
public String getColumnName() {
return columnName;
}
@Override
public String getValue(AbstractTraceCallTreeNode rowObject, Settings settings,
ServiceProvider provider) {
return rowObject.getReturnValString();
}
}
private static class SnapColumn
extends AbstractDynamicTableColumnStub<AbstractTraceCallTreeNode, Long> {
private final String columnName;
SnapColumn(String columnName) {
this.columnName = columnName;
}
@Override
public String getColumnName() {
return columnName;
}
@Override
public Long getValue(AbstractTraceCallTreeNode rowObject, Settings settings,
ServiceProvider provider) {
return rowObject.getSnapshotKey();
}
}
Set<DynamicTableColumn<AbstractTraceCallTreeNode, ?, ?>> parameterColumns = new HashSet<>();
public TraceCallTreeModel(AbstractTraceCallTreeNode rootNode) {
super(rootNode);
}
@Override
protected TableColumnDescriptor<AbstractTraceCallTreeNode> createTableColumnDescriptor() {
final TableColumnDescriptor<AbstractTraceCallTreeNode> descriptor =
super.createTableColumnDescriptor();
descriptor.addVisibleColumn(new SnapColumn("Snapshot"));
descriptor.addVisibleColumn(new ModuleColumn("Module"));
descriptor.addVisibleColumn(new ReturnColumn("Return"));
return descriptor;
}
public void setNumberOfParameterColumns(int num) {
removeTableColumns(parameterColumns);
parameterColumns.clear();
for (int i = 0; i < num; i++) {
final ParameterColumn column = new ParameterColumn(i);
parameterColumns.add(column);
}
if (!parameterColumns.isEmpty()) {
addTableColumns(parameterColumns);
}
}
@Override
protected String treeColumnName() {
return "Function";
}
}

View File

@@ -0,0 +1,90 @@
/* ###
* 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.tracecalltree;
import ghidra.app.events.CloseProgramPluginEvent;
import ghidra.app.events.OpenProgramPluginEvent;
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.TraceActivatedPluginEvent;
import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent;
import ghidra.app.services.*;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.trace.model.Trace;
@PluginInfo(
shortDescription = "Call Tree Viewer For Traces",
description = "Displays a call tree of all functions ran in a trace",
category = PluginCategoryNames.DEBUGGER,
packageName = DebuggerPluginPackage.NAME,
status = PluginStatus.UNSTABLE,
servicesRequired = { DebuggerTraceManagerService.class, DebuggerStaticMappingService.class,
DebuggerListingService.class, ProgressService.class },
eventsConsumed = { TraceClosedPluginEvent.class, TraceActivatedPluginEvent.class,
OpenProgramPluginEvent.class, CloseProgramPluginEvent.class }
)
public class TraceCallTreePlugin extends AbstractDebuggerPlugin {
TraceCallTreeProvider provider;
private DebuggerCoordinates current;
public TraceCallTreePlugin(PluginTool tool) {
super(tool);
}
@Override
protected void dispose() {
tool.removeComponentProvider(provider);
provider.dispose();
super.dispose();
}
@Override
protected void init() {
super.init();
provider = new TraceCallTreeProvider(this);
}
@Override
public void processEvent(PluginEvent event) {
super.processEvent(event);
switch (event) {
case final TraceClosedPluginEvent evt -> {
final Trace t = evt.getTrace();
if ((current != null) && (current.getTrace() == t)) {
current = null;
provider.setCoordinates(null);
}
provider.traceClosed(evt.getTrace());
}
case final TraceActivatedPluginEvent evt -> {
current = evt.getActiveCoordinates();
provider.setCoordinates(current);
}
case final OpenProgramPluginEvent evt -> {
provider.programOpened(evt.getProgram());
}
case final CloseProgramPluginEvent evt -> {
provider.programClosed(evt.getProgram());
}
default -> {
}
}
}
}

View File

@@ -0,0 +1,34 @@
/* ###
* 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.tracecalltree;
import java.util.List;
import ghidra.trace.model.time.TraceSnapshot;
import resources.Icons;
public class TraceCallTreeReturnNode extends AbstractTraceCallTreeNode {
public TraceCallTreeReturnNode(String name, String module, TraceSnapshot snap,
List<ParamNameToBytes> parameters, byte[] returnVal) {
super(name, module, snap, parameters, returnVal);
icon = Icons.ARROW_UP_LEFT_ICON;
}
@Override
public String getTreeData() {
return "Return: %s".formatted(name);
}
}

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.tracecalltree;
import javax.swing.*;
import docking.util.MessageOverlay;
import docking.widgets.gtreetable.GTreeTable;
import docking.widgets.gtreetable.GTreeTableModel;
import docking.widgets.table.GTable;
public class TraceCallTreeTable extends GTreeTable<AbstractTraceCallTreeNode> {
private MessageOverlay msgOverlay;
public TraceCallTreeTable(GTreeTableModel<AbstractTraceCallTreeNode> model) {
super(model);
}
@Override
protected void addTableToPanel(GTable table) {
msgOverlay = new MessageOverlay();
final JScrollPane pane = new JScrollPane(table);
final JLayer<JComponent> layer = new JLayer<>(pane, msgOverlay);
add(layer);
}
public void setStatusMessage(String msg) {
msgOverlay.setMessage(msg);
repaint();
}
}

View File

@@ -0,0 +1,34 @@
/* ###
* 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.tracecalltree;
import java.util.List;
import ghidra.trace.model.time.TraceSnapshot;
import resources.Icons;
public class TraceCallTreeTailCallNode extends AbstractTraceCallTreeNode {
public TraceCallTreeTailCallNode(String name, String module, TraceSnapshot snap,
List<ParamNameToBytes> parameters, byte[] returnVal) {
super(name, module, snap, parameters, returnVal);
icon = Icons.NAVIGATE_ON_INCOMING_EVENT_ICON;
}
@Override
public String getTreeData() {
return "Tail Call: %s".formatted(name);
}
}

View File

@@ -16,6 +16,7 @@
package ghidra.app.plugin.core.debug.service.modules;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.*;
@@ -30,6 +31,8 @@ import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent;
import ghidra.app.plugin.core.debug.event.TraceOpenedPluginEvent;
import ghidra.app.plugin.core.debug.service.modules.DebuggerStaticMappingContext.ChangeCollector;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.action.ByModuleAutoMapSpec;
import ghidra.app.plugin.core.debug.service.modules.DebuggerStaticMappingProposals.*;
import ghidra.app.plugin.core.debug.utils.ProgramURLUtils;
import ghidra.app.services.*;
@@ -50,8 +53,11 @@ import ghidra.trace.model.*;
import ghidra.trace.model.memory.TraceMemoryRegion;
import ghidra.trace.model.modules.*;
import ghidra.trace.model.program.TraceProgramView;
import ghidra.util.HTMLUtilities;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.VersionException;
import ghidra.util.task.Task;
import ghidra.util.task.TaskMonitor;
@PluginInfo(
@@ -79,6 +85,8 @@ public class DebuggerStaticMappingServicePlugin extends Plugin
private DebuggerTraceManagerService traceManager;
@AutoServiceConsumed
private ProgramManager programManager;
@AutoServiceConsumed
private ProgressService progressService;
@SuppressWarnings("unused")
private final AutoService.Wiring autoWiring;
@@ -119,16 +127,127 @@ public class DebuggerStaticMappingServicePlugin extends Plugin
}, executor);
}
private void checkTraceMapping(Trace trace, TaskMonitor monitor) throws CancelledException {
DebuggerAutoMappingService autoMappingService =
tool.getService(DebuggerAutoMappingService.class);
if (!(autoMappingService.getAutoMapSpec() instanceof ByModuleAutoMapSpec)) {
// TODO GP-6856: This should be factored into the ByModuleAutoMapSpec and the
// other specs should have their own implementation
return;
}
final TraceModuleManager moduleManager = trace.getModuleManager();
final DebuggerConsoleService consoleService = tool.getService(DebuggerConsoleService.class);
for (final TraceModule module : moduleManager.getAllModules()) {
for (final Lifespan lifespan : module.getObject().getLife().spans()) {
final Long firstSnap = lifespan.min();
final DomainFile match = findBestModuleProgram(
module.getMaxAddress(firstSnap).getAddressSpace(), module, firstSnap);
if (match == null) {
consoleService.log(DebuggerResources.ICON_LOG_ERROR,
"<html>The module <b><tt>" +
HTMLUtilities.escapeHTML(module.getName(firstSnap)) +
"</tt></b> was not found in the project</html>",
new DebuggerMissingModuleActionContext(module, firstSnap));
}
else {
if (match.isOpen()) {
try {
if (match.getDomainObject(this, false, false,
monitor) instanceof final Program p) {
if (!alreadyMapped(trace, firstSnap, p)) {
final ModuleMapProposal proposeModuleMap =
proposeModuleMap(module, firstSnap, p);
final Map<TraceModule, ModuleMapEntry> computeMap =
proposeModuleMap.computeMap();
addMappings(computeMap.values(), monitor, true, "");
}
}
}
catch (VersionException | IOException e) {
// This should not happen since the domain file is already open
Msg.error(this,
"Error getting domainObject: %s".formatted(e.getMessage()));
}
}
else {
consoleService.log(DebuggerResources.ICON_MODULES,
"<html>Program <b><tt>" +
HTMLUtilities.escapeHTML(match.getPathname()) +
"</tt></b> is used by " + trace.getName() +
". Open it so it can mapped.</html>",
new DebuggerOpenProgramActionContext(match));
}
}
}
}
}
private void checkAllTraceMappingsForProgram(Program program, TaskMonitor monitor)
throws CancelledException {
Iterator<Trace> iterator =
traceManager.getOpenTraces().stream().filter(t -> !t.isClosed()).iterator();
DebuggerAutoMappingService autoMappingService =
tool.getService(DebuggerAutoMappingService.class);
while (iterator.hasNext()) {
Trace trace = iterator.next();
if (!(autoMappingService.getAutoMapSpec() instanceof ByModuleAutoMapSpec)) {
// TODO GP-6856: This should be factored into the ByModuleAutoMapSpec and the
// other specs should have their own implementation
continue;
}
final TraceModuleManager moduleManager = trace.getModuleManager();
for (final TraceModule module : moduleManager.getAllModules()) {
for (final Lifespan lifespan : module.getObject().getLife().spans()) {
final Long firstSnap = lifespan.min();
if (alreadyMapped(trace, firstSnap, program)) {
continue;
}
final DomainFile match = findBestModuleProgram(
module.getMaxAddress(firstSnap).getAddressSpace(), module, firstSnap);
if (match != null && match.equals(program.getDomainFile())) {
final ModuleMapProposal proposeModuleMap =
proposeModuleMap(module, firstSnap, program);
final Map<TraceModule, ModuleMapEntry> computeMap =
proposeModuleMap.computeMap();
addMappings(computeMap.values(), monitor, true, "");
}
}
}
}
}
private boolean alreadyMapped(Trace trace, long snap, Program program) {
return getOpenMappedProgramsAtSnap(trace, snap).contains(program);
}
@Override
public void processEvent(PluginEvent event) {
if (event instanceof ProgramOpenedPluginEvent) {
if (event instanceof ProgramOpenedPluginEvent ev) {
CompletableFuture.runAsync(this::programsChanged, executor);
progressService.execute(new Task("Check trace mappings") {
@Override
public void run(TaskMonitor monitor) throws CancelledException {
checkAllTraceMappingsForProgram(ev.getProgram(), monitor);
}
});
}
else if (event instanceof ProgramClosedPluginEvent) {
CompletableFuture.runAsync(this::programsChanged, executor);
}
else if (event instanceof TraceOpenedPluginEvent) {
else if (event instanceof TraceOpenedPluginEvent ev) {
CompletableFuture.runAsync(this::tracesChanged, executor);
progressService.execute(new Task("Check trace mappings") {
@Override
public void run(TaskMonitor monitor) throws CancelledException {
checkTraceMapping(ev.getTrace(), monitor);
}
});
}
else if (event instanceof TraceClosedPluginEvent) {
CompletableFuture.runAsync(this::tracesChanged, executor);

View File

@@ -703,7 +703,7 @@ public class DynamicStaticSynchronizationPlugin extends Plugin {
consoleService.log(DebuggerResources.ICON_LOG_ERROR,
"<html>The module <b><tt>" + HTMLUtilities.escapeHTML(mod.getName(snap)) +
"</tt></b> was not found in the project</html>",
new DebuggerMissingModuleActionContext(mod));
new DebuggerMissingModuleActionContext(mod, snap));
}
/**
* Once the programs are opened, including those which are successfully imported, the

View File

@@ -67,6 +67,8 @@ color.bg.formatted.field.error = color.palette.lightcoral
color.bg.formatted.field.editing = color.bg.filterfield
color.fg.formatted.field.editing = color.fg.filterfield
color.bg.messageoverlay = color.palette.lightcornflowerblue
color.fg.messageoverlay = color.palette.white
icon.folder.new = folder_add.png
icon.toggle.expand = expand.gif
@@ -189,6 +191,7 @@ font.widget.tabs.list = sansserif-bold-9
font.wizard.border.title = sansserif-plain-10
font.messageoverlay = SansSerif-BOLDITALIC-24
[Dark Defaults]

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.util;
import java.awt.*;
import javax.swing.JComponent;
import javax.swing.plaf.LayerUI;
import generic.theme.GColor;
import generic.theme.Gui;
public class MessageOverlay extends LayerUI<JComponent> {
private final String FONT_ID = "font.messageoverlay";
private final Color backgroundColor = new GColor("color.bg.messageoverlay");
private final Color textColor = new GColor("color.fg.messageoverlay");
private String message = null;
@Override
public void paint(Graphics g, JComponent c) {
super.paint(g, c);
if (message == null) {
return;
}
final Graphics2D g2 = (Graphics2D) g.create();
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .60f));
final Font font = Gui.getFont(FONT_ID);
g2.setFont(font);
FontMetrics fm = g2.getFontMetrics(font);
final int bottomY = (int) (c.getY() + c.getHeight());
final int startX = 0;
final int startY = bottomY - 5;
final Color[] colors = { backgroundColor, new Color(0, 0, 0, 0) };
final int backgroundHeight = (fm.getHeight() * 3);
final int backgroundWidth = c.getWidth();
final int backgroundY = bottomY - backgroundHeight;
final float[] fractions = { 0.0f, .95f };
final int upperY = backgroundY;
g2.setPaint(new LinearGradientPaint(new Point(startX, startY), new Point(startX, upperY),
fractions, colors));
g2.fillRect(startX, backgroundY, backgroundWidth, backgroundHeight);
g2.setPaint(textColor);
final int centerX = (c.getWidth() - fm.stringWidth(message)) / 2;
g2.drawString(message, centerX, startY);
}
public void setMessage(String message) {
this.message = message;
}
}

View File

@@ -0,0 +1,104 @@
/* ###
* 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.gtreetable;
import java.awt.BorderLayout;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import docking.widgets.table.GTable;
import docking.widgets.table.GTableFilterPanel;
public class GTreeTable<T extends GTreeTableNode> extends JPanel {
private final GTable table;
private final GTableFilterPanel<T> filterPanel;
private final GTreeTableModel<T> tableModel;
public GTreeTable(GTreeTableModel<T> model) {
tableModel = model;
table = new GTable(tableModel);
setLayout(new BorderLayout());
addTableToPanel(table);
table.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
final int row = table.rowAtPoint(e.getPoint());
final int col = table.columnAtPoint(e.getPoint());
final GTreeTableNode selected = filterPanel.getSelectedItem();
if (selected == null) {
return;
}
if (table.getCellRenderer(row,
col) instanceof final GTreeTableCellRenderer renderer) {
final Rectangle cellRect = table.getCellRect(row, col, false);
if (renderer.inExpandIcon(selected, e.getX() - cellRect.x)) {
selected.setExpanded(!selected.isExpanded());
tableModel.reload();
}
}
}
});
filterPanel = new GTableFilterPanel<>(table, tableModel);
add(filterPanel, BorderLayout.SOUTH);
}
/**
* Sort of a hack to allow extending classes the ability to add any wrappers around the GTable
* if desired
*
* @param table
* Table to add to the panel
*/
protected void addTableToPanel(GTable table) {
add(new JScrollPane(table));
}
/**
* Get the filter panel of this table
*
* @return The filter panel
*/
public GTableFilterPanel<T> getFilterPanel() {
return filterPanel;
}
/**
* Get the GTable of this GTreeTable
*
* @return The table
*/
public GTable getTable() {
return table;
}
/**
* Get the model tied to this GTreeTable
*
* @return The model
*/
public GTreeTableModel<T> getTableModel() {
return tableModel;
}
}

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 docking.widgets.gtreetable;
import java.awt.*;
import javax.swing.*;
import javax.swing.border.Border;
import docking.widgets.table.GTableCellRenderer;
import docking.widgets.table.GTableCellRenderingData;
import ghidra.docking.settings.Settings;
import ghidra.util.table.column.GColumnRenderer;
public class GTreeTableCellRenderer<T extends GTreeTableNode> extends GTableCellRenderer
implements GColumnRenderer<T> {
private class ExpandCollapseBorder implements Border {
GTreeTableNode node;
ExpandCollapseBorder(GTreeTableNode node) {
super();
this.node = node;
}
@Override
public Insets getBorderInsets(final Component c) {
return insets;
}
@Override
public boolean isBorderOpaque() {
return false;
}
@Override
public void paintBorder(final Component c, final Graphics g, final int x, final int y,
final int width, final int height) {
if (node.hasVisibleChildren()) {
final JLabel renderer = new JLabel("", SwingConstants.RIGHT);
renderer.setIcon(node.isExpanded() ? expandedIcon : collapsedIcon);
renderer.setSize(insets.left, height);
renderer.paint(g);
}
}
}
private static final int PIXELS_PER_LEVEL = 16;
private final Insets insets = new Insets(0, 0, 0, 0);
private final Icon expandedIcon;
private final Icon collapsedIcon;
private final int maxIconWidth;
public GTreeTableCellRenderer() {
expandedIcon = UIManager.getIcon("Tree.expandedIcon");
collapsedIcon = UIManager.getIcon("Tree.collapsedIcon");
final int expandedWidth = expandedIcon == null ? 0 : expandedIcon.getIconWidth();
final int collapsedWidth = collapsedIcon == null ? 0 : collapsedIcon.getIconWidth();
maxIconWidth = Math.max(expandedWidth, collapsedWidth);
}
@Override
public String getFilterString(GTreeTableNode t, Settings settings) {
return t.getTreeData();
}
@Override
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
final JLabel renderer = (JLabel) super.getTableCellRendererComponent(data);
if (data.getRowObject() instanceof final GTreeTableNode c) {
final boolean selected = data.isSelected();
final JTable table = data.getTable();
renderer.setForeground(
selected ? table.getSelectionForeground() : table.getForeground());
renderer.setBackground(selected ? table.getSelectionBackground()
: getAlternatingBackgroundColor(table, data.getRowViewIndex()));
renderer.setFont(table.getFont());
renderer.setToolTipText(c.getName());
insets.left = maxIconWidth + (c.getLevel() * PIXELS_PER_LEVEL);
renderer.setIcon(c.getIcon());
renderer.setText(c.getTreeData());
renderer.setBorder(new ExpandCollapseBorder(c));
}
return renderer;
}
/**
* Check if x position is in the expand icon space
*
* @param node
* Node to determine how indented the expand icon is
* @param x
* X coordinate to check against
* @return true/false if X coordinate is in expand icon space
*/
public boolean inExpandIcon(GTreeTableNode node, int x) {
final int iconStart = (node.getLevel() * PIXELS_PER_LEVEL);
return ((x > iconStart) && (x < (iconStart + maxIconWidth)));
}
}

View File

@@ -0,0 +1,153 @@
/* ###
* 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.gtreetable;
import java.util.*;
import docking.widgets.table.*;
import docking.widgets.table.threaded.ThreadedTableModelStub;
import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.exception.CancelledException;
import ghidra.util.table.column.GColumnRenderer;
import ghidra.util.task.TaskMonitor;
public class GTreeTableModel<T extends GTreeTableNode> extends ThreadedTableModelStub<T> {
protected class TreeColumn extends AbstractDynamicTableColumnStub<T, GTreeTableNode> {
private final String columnName;
private final GTreeTableCellRenderer<GTreeTableNode> renderer;
public TreeColumn(String columnName) {
this.columnName = columnName;
renderer = new GTreeTableCellRenderer<>();
}
@Override
public String getColumnName() {
return columnName;
}
@Override
public GColumnRenderer<GTreeTableNode> getColumnRenderer() {
return renderer;
}
@Override
public GTreeTableNode getValue(T rowObject, Settings settings, ServiceProvider provider)
throws IllegalArgumentException {
return rowObject;
}
}
private GTreeTableNode rootNode;
public GTreeTableModel(GTreeTableNode rootNode) {
super("", null);
this.rootNode = rootNode;
}
/**
* Add the {@TreeColumn} by default, override to add additional columns
* <p>
* {@inheritDoc}
*/
@Override
protected TableColumnDescriptor<T> createTableColumnDescriptor() {
final TableColumnDescriptor<T> descriptor = new TableColumnDescriptor<>();
descriptor.addVisibleColumn(new TreeColumn(treeColumnName()));
return descriptor;
}
/**
* Any rows that match the filter have all of their ancestors added to the filtered tree to
* ensure the tree structure remains intact
* <p>
* {@inheritDoc}
*/
@Override
protected List<T> doFilter(List<T> data, TableSortingContext<T> lastSortingContext,
TaskMonitor monitor) throws CancelledException {
if ((data.isEmpty()) || !hasFilter()) {
return data;
}
final TableFilter<T> filterCopy = getTableFilter();
final Set<T> filteredList = new HashSet<>();
final List<T> returnFilteredList = new ArrayList<>();
monitor.initialize(data.size());
for (final T rowObject : data) {
if (monitor.isCancelled()) {
return new ArrayList<>(filteredList);
}
if (filterCopy.acceptsRow(rowObject)) {
filteredList.add(rowObject);
rowObject.forEachAncestor(c -> filteredList.add((T) c));
}
monitor.incrementProgress(1);
}
// Reorder them
for (final T rowObject : data) {
if (monitor.isCancelled()) {
return new ArrayList<>(filteredList);
}
if (filteredList.contains(rowObject)) {
returnFilteredList.add(rowObject);
}
}
return returnFilteredList;
}
@Override
protected void doLoad(Accumulator<T> accumulator, TaskMonitor monitor)
throws CancelledException {
if (rootNode != null) {
for (final GTreeTableNode c : rootNode.expandedDescendants()) {
monitor.checkCancelled();
if (c.isVisible()) {
accumulator.add((T) c);
}
}
}
}
/**
* Set the root node of this model
*
* @param node
* Root node to set
*/
public void setRootNode(GTreeTableNode node) {
rootNode = node;
reload();
}
/**
* Name of the tree column, allows extending classes the ability to change the name
*
* @return Tree column name
*/
protected String treeColumnName() {
return "Tree";
}
}

View File

@@ -0,0 +1,491 @@
/* ###
* 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.gtreetable;
import java.io.Serializable;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
import javax.swing.Icon;
public class GTreeTableNode implements Serializable {
private class EachAncestorIterator implements Iterator<GTreeTableNode> {
GTreeTableNode curNode = getParent();
@Override
public boolean hasNext() {
return curNode != null;
}
@Override
public GTreeTableNode next() {
final GTreeTableNode ret = curNode;
curNode = curNode.getParent();
return ret;
}
}
private class EachDecendantIterator implements Iterator<GTreeTableNode> {
final List<GTreeTableNode> nodes = new ArrayList<>(children);
@Override
public boolean hasNext() {
return !nodes.isEmpty();
}
@Override
public GTreeTableNode next() {
final GTreeTableNode curNode = nodes.removeFirst();
nodes.addAll(curNode.getChildren());
return curNode;
}
}
private class EachExpandedIterator implements Iterator<GTreeTableNode> {
final List<GTreeTableNode> nodes = new ArrayList<>(children);
@Override
public boolean hasNext() {
return !nodes.isEmpty();
}
@Override
public GTreeTableNode next() {
final GTreeTableNode curNode = nodes.removeFirst();
if (curNode.isExpanded()) {
nodes.addAll((curNode.getChildren()));
}
return curNode;
}
}
protected final String name;
protected Icon icon;
protected boolean expanded;
protected GTreeTableNode parent;
protected List<GTreeTableNode> children;
protected boolean visible;
public GTreeTableNode(final String name) {
this.name = name;
expanded = false;
children = Collections.synchronizedList(new ArrayList<>());
visible = true;
}
/**
* Add new child to this node's children
*
* @param newChild
* New child to add
*/
public void add(GTreeTableNode newChild) {
if ((newChild != null) && (newChild.getParent() == this)) {
insert(newChild, getChildCount() - 1);
}
else {
insert(newChild, getChildCount());
}
}
/**
* Get an iterable of all the ancestors of this node
*
* @return An iterable of ancestors
*/
public Iterable<GTreeTableNode> ancestors() {
return EachAncestorIterator::new;
}
/**
* Get a list of all descendants of this node in depth first search order
*
* @return List of descendants in DFS order
*/
public List<GTreeTableNode> depthFirstSearchList() {
final List<GTreeTableNode> nodes = new LinkedList<>();
final List<GTreeTableNode> result = new LinkedList<>();
result.add(this);
nodes.addAll(0, getChildren());
while (!nodes.isEmpty()) {
final GTreeTableNode curNode = nodes.removeFirst();
result.add(curNode);
nodes.addAll(0, curNode.getChildren());
}
return result;
}
/**
* Get an iterable of all the descendants of this node
*
* @return An iterable of descendants
*/
public Iterable<GTreeTableNode> descendants() {
return EachDecendantIterator::new;
}
/**
* Get an iterable of all the expanded descendants of this node
*
* @return An iterable of expanded descendants
*/
public Iterable<GTreeTableNode> expandedDescendants() {
return EachExpandedIterator::new;
}
/**
* Find nodes in descendants that match a certain condition
*
* @param condition
* Predicate to match nodes on
* @return List of nodes matching the condition
*/
public List<GTreeTableNode> find(Predicate<GTreeTableNode> condition) {
final List<GTreeTableNode> nodes = new LinkedList<>();
final List<GTreeTableNode> result = new LinkedList<>();
result.add(this);
nodes.addAll(0, getChildren());
while (!nodes.isEmpty()) {
final GTreeTableNode curNode = nodes.removeFirst();
if (condition.test(curNode)) {
result.add(curNode);
}
nodes.addAll(0, curNode.getChildren());
}
return result;
}
/**
* Perform an action on each ancestor of this node
*
* @param action
* To perform on each ancestor
*/
public void forEachAncestor(final Consumer<GTreeTableNode> action) {
GTreeTableNode curNode = getParent();
while (curNode != null) {
action.accept(curNode);
curNode = curNode.getParent();
}
}
/**
* Perform an action on each descendant of this node
*
* @param action
* To perform on each descendant
*/
public void forEachDescendant(final Consumer<GTreeTableNode> action) {
final List<GTreeTableNode> nodes = new ArrayList<>(children);
while (!nodes.isEmpty()) {
final GTreeTableNode curNode = nodes.removeFirst();
action.accept(curNode);
nodes.addAll(curNode.getChildren());
}
}
/**
* Perform an action on each expanded descendant of this node
*
* @param action
* To perform on each expanded descendant
*/
public void forEachExpanded(final Consumer<GTreeTableNode> action) {
final List<GTreeTableNode> nodes = new ArrayList<>(children);
while (!nodes.isEmpty()) {
final GTreeTableNode curNode = nodes.removeFirst();
action.accept(curNode);
if (curNode.isExpanded()) {
nodes.addAll((curNode.getChildren()));
}
}
}
/**
* Get number of children
*
* @return Number of children
*/
public int getChildCount() {
return children.size();
}
/**
* Get list of children
*
* @return List of children
*/
public List<GTreeTableNode> getChildren() {
return children;
}
/**
* Get list of descendants who are expanded
*
* @return List of expanded descendants
*/
public List<GTreeTableNode> getExpanded() {
final List<GTreeTableNode> nodes = new ArrayList<>();
forEachExpanded(nodes::add);
return nodes;
}
/**
* Get icon
*
* @return icon
*/
public Icon getIcon() {
return icon;
}
/**
* Get tree depth from the root node
*
* @return Level from root node
*/
public int getLevel() {
int levels = 0;
GTreeTableNode ancestor = this;
while ((ancestor = ancestor.getParent()) != null) {
++levels;
}
return levels;
}
/**
* Get name of node
*
* @return name
*/
public String getName() {
return name;
}
/**
* Get parent of node
*
* @return parent
*/
public GTreeTableNode getParent() {
return parent;
}
/**
* Get list of ancestors in order leading to this node
*
* @return List of nodes leading to this one
*/
public List<GTreeTableNode> getPath() {
final List<GTreeTableNode> path = new ArrayList<>();
forEachAncestor(path::add);
return path;
}
/**
* Get root node
*
* @return root node
*/
public GTreeTableNode getRoot() {
for (GTreeTableNode cur = this;; cur = cur.getParent()) {
if (cur.getParent() == null) {
return cur;
}
}
}
/**
* Get data associated with this node
*
* @return data
*/
public String getTreeData() {
return name;
}
/**
* Check if node has children
*
* @return true/false if node has children
*/
public boolean hasChildren() {
return (children != null) && !children.isEmpty();
}
/**
* Check if another node is in this node's ancestry, the current node will return true as
* being in its own ancestry
*
* @param anotherNode
* Node to check for in the current nodes ancestry
* @return true/false if other node is in the ancestry
*/
public boolean hasNodeInItsAncestry(GTreeTableNode anotherNode) {
if (anotherNode == null) {
return false;
}
for (GTreeTableNode cur = this; cur != null; cur = cur.getParent()) {
if (anotherNode == cur) {
return true;
}
}
return false;
}
/**
* Check if this node has any visible children
*
* @return true/false if any children are visible
*/
public boolean hasVisibleChildren() {
return (children != null) && children.stream().anyMatch(c -> c.visible);
}
/**
* Insert a new node into the children of this node
*
* @param newChild
* New child to insert
* @param childIndex
* Index to insert child at
*/
public void insert(GTreeTableNode newChild, int childIndex) {
if (hasNodeInItsAncestry(Objects.requireNonNull(newChild))) {
throw new IllegalArgumentException(
"New child is already an ancestor, and cycles are not permitted");
}
final GTreeTableNode oldParent = newChild.getParent();
if (oldParent != null) {
oldParent.remove(newChild);
}
newChild.setParent(this);
if (children == null) {
children = Collections.synchronizedList(new ArrayList<>());
}
children.add(childIndex, newChild);
}
/**
* Check if node is expanded
*
* @return true/false if expanded
*/
public boolean isExpanded() {
return expanded;
}
/**
* Check if node is leaf node
*
* @return true/false if leaf
*/
public boolean isLeaf() {
return children.isEmpty();
}
/**
* Check if node is root node (i.e. it has no parent)
*
* @return true/false if root
*/
public boolean isRoot() {
return getParent() == null;
}
/**
* @return
*/
public boolean isVisible() {
return visible;
}
/**
* Remove child from list of children
*
* @param aChild
* Child to remove
*/
public void remove(GTreeTableNode aChild) {
children.remove(aChild);
aChild.setParent(null);
}
/**
* Remove child from list of children
*
* @param childIndex
* Index of child to remove
*/
public void remove(int childIndex) {
final GTreeTableNode child = children.get(childIndex);
children.remove(childIndex);
child.setParent(null);
}
/**
* Change node expanded state
*
* @param expanded
* Expanded state
*/
public void setExpanded(boolean expanded) {
this.expanded = expanded;
}
/**
* Set parent of node
*
* @param newParent
* Parent to set
*/
public void setParent(GTreeTableNode newParent) {
parent = newParent;
}
/**
* Change node visibility state
*
* @param visible
* Node visibility
*/
public void setVisible(boolean visible) {
this.visible = visible;
}
@Override
public String toString() {
return name == null ? "" : name;
}
}