GP-1808: Added 'Run to Address'-type actions to right-click menu for some connectors.

This commit is contained in:
Dan 2023-02-07 12:23:16 -05:00
parent 44d7c4f031
commit bde529b4d5
39 changed files with 1663 additions and 136 deletions

View file

@ -15,16 +15,30 @@
*/
package ghidra.dbg.target;
import java.lang.annotation.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.reflect.TypeUtils;
import ghidra.dbg.DebuggerTargetObjectIface;
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
import ghidra.dbg.agent.DefaultTargetObject;
import ghidra.dbg.error.DebuggerIllegalArgumentException;
import ghidra.dbg.target.TargetMethod.*;
import ghidra.dbg.target.TargetMethod.TargetParameterMap.EmptyTargetParameterMap;
import ghidra.dbg.target.TargetMethod.TargetParameterMap.ImmutableTargetParameterMap;
import ghidra.dbg.target.schema.TargetAttributeType;
import ghidra.dbg.util.CollectionUtils.AbstractEmptyMap;
import ghidra.dbg.util.CollectionUtils.AbstractNMap;
import utilities.util.reflection.ReflectionUtilities;
/**
* An object which can be invoked as a method
@ -38,11 +52,207 @@ public interface TargetMethod extends TargetObject {
String RETURN_TYPE_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "return_type";
public static String REDIRECT = "<=";
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface Export {
String value();
}
interface Value<T> {
boolean specified();
T value();
}
@interface BoolValue {
boolean specified() default true;
boolean value();
record Val(BoolValue v) implements Value<Boolean> {
@Override
public boolean specified() {
return v.specified();
}
@Override
public Boolean value() {
return v.value();
}
}
}
@interface IntValue {
boolean specified() default true;
int value();
record Val(IntValue v) implements Value<Integer> {
@Override
public boolean specified() {
return v.specified();
}
@Override
public Integer value() {
return v.value();
}
}
}
@interface LongValue {
boolean specified() default true;
long value();
record Val(LongValue v) implements Value<Long> {
@Override
public boolean specified() {
return v.specified();
}
@Override
public Long value() {
return v.value();
}
}
}
@interface FloatValue {
boolean specified() default true;
float value();
record Val(FloatValue v) implements Value<Float> {
@Override
public boolean specified() {
return v.specified();
}
@Override
public Float value() {
return v.value();
}
}
}
@interface DoubleValue {
boolean specified() default true;
double value();
record Val(DoubleValue v) implements Value<Double> {
@Override
public boolean specified() {
return v.specified();
}
@Override
public Double value() {
return v.value();
}
}
}
@interface BytesValue {
boolean specified() default true;
byte[] value();
record Val(BytesValue v) implements Value<byte[]> {
@Override
public boolean specified() {
return v.specified();
}
@Override
public byte[] value() {
return v.value();
}
}
}
@interface StringValue {
boolean specified() default true;
String value();
record Val(StringValue v) implements Value<String> {
@Override
public boolean specified() {
return v.specified();
}
@Override
public String value() {
return v.value();
}
}
}
@interface StringsValue {
boolean specified() default true;
String[] value();
record Val(StringsValue v) implements Value<List<String>> {
@Override
public boolean specified() {
return v.specified();
}
@Override
public List<String> value() {
return List.of(v.value());
}
}
}
// TODO: Address, Range, BreakKind[Set], etc?
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@interface Param {
List<Function<Param, Value<?>>> DEFAULTS = List.of(
p -> new BoolValue.Val(p.defaultBool()),
p -> new IntValue.Val(p.defaultInt()),
p -> new LongValue.Val(p.defaultLong()),
p -> new FloatValue.Val(p.defaultFloat()),
p -> new DoubleValue.Val(p.defaultDouble()),
p -> new BytesValue.Val(p.defaultBytes()),
p -> new StringValue.Val(p.defaultString()));
String name();
String display();
String description();
// TODO: Something that hints at changes in activation?
boolean required() default true;
BoolValue defaultBool() default @BoolValue(specified = false, value = false);
IntValue defaultInt() default @IntValue(specified = false, value = 0);
LongValue defaultLong() default @LongValue(specified = false, value = 0);
FloatValue defaultFloat() default @FloatValue(specified = false, value = 0);
DoubleValue defaultDouble() default @DoubleValue(specified = false, value = 0);
BytesValue defaultBytes() default @BytesValue(specified = false, value = {});
StringValue defaultString() default @StringValue(specified = false, value = "");
StringsValue choicesString() default @StringsValue(specified = false, value = {});
}
/**
* A description of a method parameter
*
* <p>
* TODO: For convenience, these should be programmable via annotations.
* <P>
* TODO: Should this be incorporated into schemas?
*
@ -85,6 +295,83 @@ public interface TargetMethod extends TargetObject {
choices);
}
protected static boolean isRequired(Class<?> type, Param param) {
if (!type.isPrimitive()) {
return param.required();
}
if (type == boolean.class) {
return !param.defaultBool().specified();
}
if (type == int.class) {
return !param.defaultInt().specified();
}
if (type == long.class) {
return !param.defaultLong().specified();
}
if (type == float.class) {
return !param.defaultFloat().specified();
}
if (type == double.class) {
return !param.defaultDouble().specified();
}
throw new IllegalArgumentException("Parameter type not allowed: " + type);
}
protected static Object getDefault(Param annot) {
List<Object> defaults = new ArrayList<>();
for (Function<Param, Value<?>> df : Param.DEFAULTS) {
Value<?> value = df.apply(annot);
if (value.specified()) {
defaults.add(value.value());
}
}
if (defaults.isEmpty()) {
return null;
}
if (defaults.size() > 1) {
throw new IllegalArgumentException(
"Can only specify one default value. Got " + defaults);
}
return defaults.get(0);
}
protected static <T> T getDefault(Class<T> type, Param annot) {
Object dv = getDefault(annot);
if (dv == null) {
return null;
}
if (!type.isInstance(dv)) {
throw new IllegalArgumentException(
"Type of default does not match that of parameter. Expected type " + type +
". Got (" + dv.getClass() + ")" + dv);
}
return type.cast(dv);
}
protected static <T> ParameterDescription<T> annotated(Class<T> type, Param annot) {
boolean required = isRequired(type, annot);
T defaultValue = getDefault(type, annot);
return ParameterDescription.create(type, annot.name(),
required, defaultValue, annot.display(), annot.description());
}
public static ParameterDescription<?> annotated(Parameter parameter) {
Param annot = parameter.getAnnotation(Param.class);
if (annot == null) {
throw new IllegalArgumentException(
"Missing @" + Param.class.getSimpleName() + " on " + parameter);
}
if (annot.choicesString().specified()) {
if (parameter.getType() != String.class) {
throw new IllegalArgumentException(
"Can only specify choices for String parameter");
}
return ParameterDescription.choices(String.class, annot.name(),
List.of(annot.choicesString().value()), annot.display(), annot.description());
}
return annotated(MethodType.methodType(parameter.getType()).wrap().returnType(), annot);
}
public final Class<T> type;
public final String name;
public final T defaultValue;
@ -199,13 +486,78 @@ public interface TargetMethod extends TargetObject {
public static TargetParameterMap ofEntries(
Entry<String, ParameterDescription<?>>... entries) {
Map<String, ParameterDescription<?>> ordered = new LinkedHashMap<>();
for (Entry<String, ParameterDescription<?>> ent: entries) {
for (Entry<String, ParameterDescription<?>> ent : entries) {
ordered.put(ent.getKey(), ent.getValue());
}
return new ImmutableTargetParameterMap(ordered);
}
}
class AnnotatedTargetMethod extends DefaultTargetObject<TargetObject, TargetObject>
implements TargetMethod {
public static Map<String, AnnotatedTargetMethod> collectExports(Lookup lookup,
AbstractDebuggerObjectModel model, TargetObject parent) {
Map<String, AnnotatedTargetMethod> result = new HashMap<>();
Set<Class<?>> allClasses = new LinkedHashSet<>();
allClasses.add(parent.getClass());
allClasses.addAll(ReflectionUtilities.getAllParents(parent.getClass()));
for (Class<?> declCls : allClasses) {
for (Method method : declCls.getDeclaredMethods()) {
Export annot = method.getAnnotation(Export.class);
if (annot == null || result.containsKey(annot.value())) {
continue;
}
result.put(annot.value(),
new AnnotatedTargetMethod(lookup, model, parent, method, annot));
}
}
return result;
}
private final MethodHandle handle;
private final TargetParameterMap params;
public AnnotatedTargetMethod(Lookup lookup, AbstractDebuggerObjectModel model,
TargetObject parent, Method method, Export annot) {
super(model, parent, annot.value(), "Method");
try {
this.handle = lookup.unreflect(method).bindTo(parent);
}
catch (IllegalAccessException e) {
throw new IllegalArgumentException("Method " + method + " is not accessible");
}
this.params = TargetMethod.makeParameters(
Stream.of(method.getParameters()).map(ParameterDescription::annotated));
Map<TypeVariable<?>, Type> argsCf = TypeUtils
.getTypeArguments(method.getGenericReturnType(), CompletableFuture.class);
Type typeCfT = argsCf.get(CompletableFuture.class.getTypeParameters()[0]);
Class<?> returnType = TypeUtils.getRawType(typeCfT, typeCfT);
changeAttributes(List.of(), Map.ofEntries(
Map.entry(RETURN_TYPE_ATTRIBUTE_NAME, returnType),
Map.entry(PARAMETERS_ATTRIBUTE_NAME, params)),
"Initialize");
}
@SuppressWarnings("unchecked")
@Override
public CompletableFuture<Object> invoke(Map<String, ?> arguments) {
Map<String, ?> valid = TargetMethod.validateArguments(params, arguments, false);
List<Object> args = new ArrayList<>(params.size());
for (ParameterDescription<?> p : params.values()) {
args.add(p.get(valid));
}
try {
return (CompletableFuture<Object>) handle.invokeWithArguments(args);
}
catch (Throwable e) {
return CompletableFuture.failedFuture(e);
}
}
}
/**
* Construct a map of parameter descriptions from a stream
*

View file

@ -66,31 +66,12 @@ public interface TargetSteppable extends TargetObject {
}
enum TargetStepKind {
/**
* Step strictly forward
*
* <p>
* To avoid runaway execution, stepping should cease if execution returns from the current
* frame.
*
* <p>
* In more detail: step until execution reaches the instruction following this one,
* regardless of the current frame. This differs from {@link #UNTIL} in that it doesn't
* regard the current frame.
*/
ADVANCE,
/**
* Step out of the current function.
*
* <p>
* In more detail: step until the object has executed the return instruction that returns
* from the current frame.
*
* <p>
* TODO: This step is geared toward GDB's {@code advance}, which actually takes a parameter.
* Perhaps this API should adjust to accommodate stepping parameters. Would probably want a
* strict set of forms, though, and a given kind should have the same form everywhere. If we
* do that, then we could do nifty pop-up actions, like "Step: Advance to here".
*/
FINISH,
/**

View file

@ -25,6 +25,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.TypeUtils;
import ghidra.dbg.DebuggerTargetObjectIface;
import ghidra.dbg.target.TargetMethod;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema;
import ghidra.dbg.target.schema.EnumerableTargetObjectSchema.MinimalSchemaContext;
@ -198,6 +199,51 @@ public class AnnotatedSchemaContext extends DefaultSchemaContext {
}
}
protected void addExportedTargetMethodsFromClass(SchemaBuilder builder,
Class<? extends TargetObject> declCls, Class<? extends TargetObject> cls) {
for (Method declMethod : declCls.getDeclaredMethods()) {
int mod = declMethod.getModifiers();
if (!Modifier.isPublic(mod)) {
continue;
}
TargetMethod.Export export = declMethod.getAnnotation(TargetMethod.Export.class);
if (export == null) {
continue;
}
AttributeSchema exists = builder.getAttributeSchema(export.value());
if (exists != null) {
continue;
}
SchemaName snMethod = new SchemaName("Method");
if (getSchemaOrNull(snMethod) == null) {
builder(snMethod)
.addInterface(TargetMethod.class)
.setDefaultElementSchema(EnumerableTargetObjectSchema.VOID.getName())
.addAttributeSchema(
new DefaultAttributeSchema(TargetObject.DISPLAY_ATTRIBUTE_NAME,
EnumerableTargetObjectSchema.STRING.getName(), true, true, true),
"default")
.addAttributeSchema(
new DefaultAttributeSchema(TargetMethod.RETURN_TYPE_ATTRIBUTE_NAME,
EnumerableTargetObjectSchema.TYPE.getName(), true, true, true),
"default")
.addAttributeSchema(
new DefaultAttributeSchema(TargetMethod.PARAMETERS_ATTRIBUTE_NAME,
EnumerableTargetObjectSchema.MAP_PARAMETERS.getName(), true, true,
true),
"default")
.setDefaultAttributeSchema(AttributeSchema.DEFAULT_VOID)
.buildAndAdd();
}
builder.addAttributeSchema(
new DefaultAttributeSchema(export.value(), snMethod, true, true, true), declMethod);
}
}
protected TargetObjectSchema fromAnnotatedClass(Class<? extends TargetObject> cls) {
synchronized (namesByClass) {
SchemaName name = nameFromAnnotatedClass(cls);
@ -268,27 +314,24 @@ public class AnnotatedSchemaContext extends DefaultSchemaContext {
throw new IllegalArgumentException(
"Could not identify unique element class (" + bounds + ") for " + cls);
}
else {
Class<? extends TargetObject> bound = bounds.iterator().next();
SchemaName schemaName;
try {
schemaName = nameFromClass(bound);
}
catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
"Could not get schema name from bound " + bound + " of " + cls +
".fetchElements()",
e);
}
builder.setDefaultElementSchema(schemaName);
Class<? extends TargetObject> bound = bounds.iterator().next();
SchemaName schemaName;
try {
schemaName = nameFromClass(bound);
}
catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
"Could not get schema name from bound " + bound + " of " + cls +
".fetchElements()",
e);
}
builder.setDefaultElementSchema(schemaName);
}
addPublicMethodsFromClass(builder, cls, cls);
for (Class<?> parent : allParents) {
if (TargetObject.class.isAssignableFrom(parent)) {
addPublicMethodsFromClass(builder, parent.asSubclass(TargetObject.class),
cls);
addPublicMethodsFromClass(builder, parent.asSubclass(TargetObject.class), cls);
}
}
for (TargetAttributeType at : info.attributes()) {
@ -299,6 +342,13 @@ public class AnnotatedSchemaContext extends DefaultSchemaContext {
}
builder.replaceAttributeSchema(attrSchema, at);
}
addExportedTargetMethodsFromClass(builder, cls, cls);
for (Class<?> parent : allParents) {
if (TargetObject.class.isAssignableFrom(parent)) {
addExportedTargetMethodsFromClass(builder, parent.asSubclass(TargetObject.class),
cls);
}
}
return builder;
}

View file

@ -39,6 +39,10 @@ public class DefaultSchemaContext implements SchemaContext {
schemas.put(schema.getName(), schema);
}
public synchronized void replaceSchema(TargetObjectSchema schema) {
schemas.put(schema.getName(), schema);
}
@Override
public synchronized TargetObjectSchema getSchemaOrNull(SchemaName name) {
return schemas.get(name);

View file

@ -24,7 +24,7 @@ public class DefaultTargetObjectSchema
implements TargetObjectSchema, Comparable<DefaultTargetObjectSchema> {
private static final String INDENT = " ";
protected static class DefaultAttributeSchema
public static class DefaultAttributeSchema
implements AttributeSchema, Comparable<DefaultAttributeSchema> {
private final String name;
private final SchemaName schema;

View file

@ -36,7 +36,7 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema {
* <p>
* The described value can be any primitive or a {@link TargetObject}.
*/
ANY("ANY", Object.class) {
ANY(Object.class) {
@Override
public SchemaName getDefaultElementSchema() {
return OBJECT.getName();
@ -53,7 +53,7 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema {
* <p>
* This requires nothing more than the described value to be a {@link TargetObject}.
*/
OBJECT("OBJECT", TargetObject.class) {
OBJECT(TargetObject.class) {
@Override
public SchemaName getDefaultElementSchema() {
return OBJECT.getName();
@ -64,6 +64,7 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema {
return AttributeSchema.DEFAULT_ANY;
}
},
TYPE(Class.class),
/**
* A type so restrictive nothing can satisfy it.
*
@ -72,22 +73,23 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema {
* the default attribute when only certain enumerated attributes are allowed. It is also used as
* the type for the children of primitives, since primitives cannot have successors.
*/
VOID("VOID", Void.class, void.class),
BOOL("BOOL", Boolean.class, boolean.class),
BYTE("BYTE", Byte.class, byte.class),
SHORT("SHORT", Short.class, short.class),
INT("INT", Integer.class, int.class),
LONG("LONG", Long.class, long.class),
STRING("STRING", String.class),
ADDRESS("ADDRESS", Address.class),
RANGE("RANGE", AddressRange.class),
DATA_TYPE("DATA_TYPE", TargetDataType.class),
LIST_OBJECT("LIST_OBJECT", TargetObjectList.class),
MAP_PARAMETERS("MAP_PARAMETERS", TargetParameterMap.class),
SET_ATTACH_KIND("SET_ATTACH_KIND", TargetAttachKindSet.class), // TODO: Limited built-in generics
SET_BREAKPOINT_KIND("SET_BREAKPOINT_KIND", TargetBreakpointKindSet.class),
SET_STEP_KIND("SET_STEP_KIND", TargetStepKindSet.class),
EXECUTION_STATE("EXECUTION_STATE", TargetExecutionState.class);
VOID(Void.class, void.class),
BOOL(Boolean.class, boolean.class),
BYTE(Byte.class, byte.class),
SHORT(Short.class, short.class),
INT(Integer.class, int.class),
LONG(Long.class, long.class),
STRING(String.class),
ADDRESS(Address.class),
RANGE(AddressRange.class),
DATA_TYPE(TargetDataType.class),
// TODO: Limited built-in generics?
LIST_OBJECT(TargetObjectList.class),
MAP_PARAMETERS(TargetParameterMap.class),
SET_ATTACH_KIND(TargetAttachKindSet.class),
SET_BREAKPOINT_KIND(TargetBreakpointKindSet.class),
SET_STEP_KIND(TargetStepKindSet.class),
EXECUTION_STATE(TargetExecutionState.class);
public static final class MinimalSchemaContext extends DefaultSchemaContext {
public static final SchemaContext INSTANCE = new MinimalSchemaContext();
@ -126,8 +128,8 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema {
private final SchemaName name;
private final List<Class<?>> types;
private EnumerableTargetObjectSchema(String name, Class<?>... types) {
this.name = new SchemaName(name);
private EnumerableTargetObjectSchema(Class<?>... types) {
this.name = new SchemaName(this.name());
this.types = List.of(types);
}

View file

@ -44,6 +44,21 @@ public class SchemaBuilder {
this.name = name;
}
public SchemaBuilder(DefaultSchemaContext context, TargetObjectSchema schema) {
this(context, schema.getName());
setType(schema.getType());
setInterfaces(schema.getInterfaces());
setCanonicalContainer(schema.isCanonicalContainer());
elementSchemas.putAll(schema.getElementSchemas());
setDefaultElementSchema(schema.getDefaultElementSchema());
setElementResyncMode(schema.getElementResyncMode());
attributeSchemas.putAll(schema.getAttributeSchemas());
setDefaultAttributeSchema(schema.getDefaultAttributeSchema());
setAttributeResyncMode(schema.getAttributeResyncMode());
}
public SchemaBuilder setType(Class<?> type) {
this.type = type;
return this;

View file

@ -478,7 +478,8 @@ public interface TargetObjectSchema {
throw new IllegalArgumentException("Must provide a specific interface");
}
PathMatcher result = new PathMatcher();
Private.searchFor(this, result, prefix, true, type, requireCanonical, new HashSet<>());
Private.searchFor(this, result, prefix, true, type, false, requireCanonical,
new HashSet<>());
return result;
}
@ -610,37 +611,44 @@ public interface TargetObjectSchema {
private static void searchFor(TargetObjectSchema sch, PathMatcher result,
List<String> prefix, boolean parentIsCanonical, Class<? extends TargetObject> type,
boolean requireCanonical, Set<TargetObjectSchema> visited) {
boolean requireAggregate, boolean requireCanonical,
Set<TargetObjectSchema> visited) {
if (sch instanceof EnumerableTargetObjectSchema) {
return;
}
if (sch.getInterfaces().contains(type) && (parentIsCanonical || !requireCanonical)) {
result.addPattern(prefix);
}
if (!visited.add(sch)) {
return;
}
if (sch.getInterfaces().contains(type) && parentIsCanonical) {
result.addPattern(prefix);
if (requireAggregate && !sch.getInterfaces().contains(TargetAggregate.class)) {
return;
}
SchemaContext ctx = sch.getContext();
boolean isCanonical = sch.isCanonicalContainer();
for (Entry<String, SchemaName> ent : sch.getElementSchemas().entrySet()) {
List<String> extended = PathUtils.index(prefix, ent.getKey());
TargetObjectSchema elemSchema = ctx.getSchema(ent.getValue());
searchFor(elemSchema, result, extended, isCanonical, type, requireCanonical,
visited);
searchFor(elemSchema, result, extended, isCanonical, type, requireAggregate,
requireCanonical, visited);
}
List<String> deExtended = PathUtils.extend(prefix, "[]");
TargetObjectSchema deSchema = ctx.getSchema(sch.getDefaultElementSchema());
searchFor(deSchema, result, deExtended, isCanonical, type, requireCanonical, visited);
searchFor(deSchema, result, deExtended, isCanonical, type, requireAggregate,
requireCanonical, visited);
for (Entry<String, AttributeSchema> ent : sch.getAttributeSchemas().entrySet()) {
List<String> extended = PathUtils.extend(prefix, ent.getKey());
TargetObjectSchema attrSchema = ctx.getSchema(ent.getValue().getSchema());
searchFor(attrSchema, result, extended, parentIsCanonical, type, requireCanonical,
visited);
searchFor(attrSchema, result, extended, parentIsCanonical, type, requireAggregate,
requireCanonical, visited);
}
List<String> daExtended = PathUtils.extend(prefix, "");
TargetObjectSchema daSchema =
ctx.getSchema(sch.getDefaultAttributeSchema().getSchema());
searchFor(daSchema, result, daExtended, parentIsCanonical, type, requireCanonical,
visited);
searchFor(daSchema, result, daExtended, parentIsCanonical, type, requireAggregate,
requireCanonical, visited);
visited.remove(sch);
}
@ -774,6 +782,34 @@ public interface TargetObjectSchema {
return null;
}
/**
* Search for all suitable objects with this schema at the given path
*
* <p>
* This behaves like {@link #searchForSuitable(Class, List)}, except that it returns a matcher
* for all possibilities. Conventionally, when the client uses the matcher to find suitable
* objects and must choose from among the results, those having the longer paths should be
* preferred. More specifically, it should prefer those sharing the longer path prefixes with
* the given path. The client should <em>not</em> just take the first objects, since these will
* likely have the shortest paths. If exactly one object is required, consider using
* {@link #searchForSuitable(Class, List)} instead.
*
* @param type
* @param path
* @return
*/
default PathPredicates matcherForSuitable(Class<? extends TargetObject> type,
List<String> path) {
PathMatcher result = new PathMatcher();
Set<TargetObjectSchema> visited = new HashSet<>();
List<TargetObjectSchema> schemas = getSuccessorSchemas(path);
for (; path != null; path = PathUtils.parent(path)) {
TargetObjectSchema schema = schemas.get(path.size());
Private.searchFor(schema, result, path, false, type, true, false, visited);
}
return result;
}
/**
* Like {@link #searchForSuitable(Class, List)}, but searches for the canonical container whose
* elements have the given type

View file

@ -327,28 +327,42 @@ public interface PathPredicates {
}
if (successorCouldMatch(path, true)) {
Set<String> nextNames = getNextNames(path);
if (!nextNames.isEmpty()) {
if (nextNames.equals(PathMatcher.WILD_SINGLETON)) {
for (Map.Entry<String, ?> ent : cur.getCachedAttributes().entrySet()) {
Object value = ent.getValue();
if (!(value instanceof TargetObject)) {
if (!(ent.getValue() instanceof TargetObject obj)) {
continue;
}
String name = ent.getKey();
if (!anyMatches(nextNames, name)) {
continue;
}
TargetObject obj = (TargetObject) value;
getCachedSuccessors(result, PathUtils.extend(path, name), obj);
}
}
else {
for (String name : nextNames) {
if (!(cur.getCachedAttribute(name) instanceof TargetObject obj)) {
continue;
}
getCachedSuccessors(result, PathUtils.extend(path, name), obj);
}
}
Set<String> nextIndices = getNextIndices(path);
if (!nextIndices.isEmpty()) {
if (nextIndices.equals(PathMatcher.WILD_SINGLETON)) {
for (Map.Entry<String, ? extends TargetObject> ent : cur.getCachedElements()
.entrySet()) {
TargetObject obj = ent.getValue();
if (obj == null) {
return;
}
String index = ent.getKey();
if (!anyMatches(nextIndices, index)) {
continue;
getCachedSuccessors(result, PathUtils.index(path, index), obj);
}
}
else {
Map<String, ? extends TargetObject> elements = cur.getCachedElements();
for (String index : nextIndices) {
TargetObject obj = elements.get(index);
if (obj == null) {
return;
}
getCachedSuccessors(result, PathUtils.index(path, index), obj);
}

View file

@ -70,7 +70,7 @@ public class TestDebuggerObjectModel extends EmptyDebuggerObjectModel {
}
protected TestTargetSession newTestTargetSession(String rootHint) {
return new TestTargetSession(this, rootHint, ROOT_SCHEMA);
return new TestTargetSession(this, rootHint, getRootSchema());
}
protected TestTargetEnvironment newTestTargetEnvironment(TestTargetSession session) {

View file

@ -0,0 +1,251 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.dbg.target;
import static org.junit.Assert.*;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import org.junit.Test;
import ghidra.async.AsyncTestUtils;
import ghidra.async.AsyncUtils;
import ghidra.dbg.error.DebuggerIllegalArgumentException;
import ghidra.dbg.model.*;
import ghidra.dbg.target.TargetMethod.*;
public class TargetMethodTest implements AsyncTestUtils {
@Test
public void testAnnotatedMethodVoid0Args() throws Throwable {
TestDebuggerModelBuilder mb = new TestDebuggerModelBuilder() {
@Override
protected TestDebuggerObjectModel newModel(String typeHint) {
return new TestDebuggerObjectModel(typeHint) {
@Override
protected TestTargetThread newTestTargetThread(
TestTargetThreadContainer container, int tid) {
return new TestTargetThread(container, tid) {
{
changeAttributes(List.of(),
AnnotatedTargetMethod.collectExports(MethodHandles.lookup(),
testModel, this),
"Methods");
}
@TargetMethod.Export("MyMethod")
public CompletableFuture<Void> myMethod() {
return AsyncUtils.NIL;
}
};
}
};
}
};
mb.createTestModel();
mb.createTestProcessesAndThreads();
TargetMethod method = (TargetMethod) mb.testThread1.getCachedAttribute("MyMethod");
assertEquals(Void.class, method.getReturnType());
assertEquals(TargetParameterMap.of(), method.getParameters());
assertNull(waitOn(method.invoke(Map.of())));
try {
waitOn(method.invoke(Map.ofEntries(Map.entry("p1", "err"))));
fail("Didn't catch extraneous argument");
}
catch (DebuggerIllegalArgumentException e) {
// pass
}
}
@Test
public void testAnnotatedMethodVoid1ArgBool() throws Throwable {
TestDebuggerModelBuilder mb = new TestDebuggerModelBuilder() {
@Override
protected TestDebuggerObjectModel newModel(String typeHint) {
return new TestDebuggerObjectModel(typeHint) {
@Override
protected TestTargetThread newTestTargetThread(
TestTargetThreadContainer container, int tid) {
return new TestTargetThread(container, tid) {
{
changeAttributes(List.of(),
AnnotatedTargetMethod.collectExports(MethodHandles.lookup(),
testModel, this),
"Methods");
}
@TargetMethod.Export("MyMethod")
public CompletableFuture<Void> myMethod(
@TargetMethod.Param(
display = "P1",
description = "A boolean param",
name = "p1") boolean b) {
return AsyncUtils.NIL;
}
};
}
};
}
};
mb.createTestModel();
mb.createTestProcessesAndThreads();
TargetMethod method = (TargetMethod) mb.testThread1.getCachedAttribute("MyMethod");
assertEquals(Void.class, method.getReturnType());
assertEquals(TargetParameterMap.ofEntries(
Map.entry("p1",
ParameterDescription.create(Boolean.class, "p1", true, null, "P1",
"A boolean param"))),
method.getParameters());
assertNull(waitOn(method.invoke(Map.ofEntries(Map.entry("p1", true)))));
try {
waitOn(method.invoke(Map.ofEntries(Map.entry("p1", "err"))));
fail("Didn't catch type mismatch");
}
catch (DebuggerIllegalArgumentException e) {
// pass
}
try {
waitOn(method.invoke(Map.ofEntries(
Map.entry("p1", true),
Map.entry("p2", "err"))));
fail("Didn't catch extraneous argument");
}
catch (DebuggerIllegalArgumentException e) {
// pass
}
try {
waitOn(method.invoke(Map.ofEntries()));
fail("Didn't catch missing argument");
}
catch (DebuggerIllegalArgumentException e) {
// pass
}
}
@Test
public void testAnnotatedMethodString1ArgInt() throws Throwable {
TestDebuggerModelBuilder mb = new TestDebuggerModelBuilder() {
@Override
protected TestDebuggerObjectModel newModel(String typeHint) {
return new TestDebuggerObjectModel(typeHint) {
@Override
protected TestTargetThread newTestTargetThread(
TestTargetThreadContainer container, int tid) {
return new TestTargetThread(container, tid) {
{
changeAttributes(List.of(),
AnnotatedTargetMethod.collectExports(MethodHandles.lookup(),
testModel, this),
"Methods");
}
@TargetMethod.Export("MyMethod")
public CompletableFuture<String> myMethod(
@TargetMethod.Param(
display = "P1",
description = "An int param",
name = "p1") int i) {
return CompletableFuture.completedFuture(Integer.toString(i));
}
};
}
};
}
};
mb.createTestModel();
mb.createTestProcessesAndThreads();
TargetMethod method = (TargetMethod) mb.testThread1.getCachedAttribute("MyMethod");
assertEquals(String.class, method.getReturnType());
assertEquals(TargetParameterMap.ofEntries(
Map.entry("p1",
ParameterDescription.create(Integer.class, "p1", true, null, "P1",
"An int param"))),
method.getParameters());
assertEquals("3", waitOn(method.invoke(Map.ofEntries(Map.entry("p1", 3)))));
}
@Test
public void testAnnotatedMethodStringManyArgs() throws Throwable {
TestDebuggerModelBuilder mb = new TestDebuggerModelBuilder() {
@Override
protected TestDebuggerObjectModel newModel(String typeHint) {
return new TestDebuggerObjectModel(typeHint) {
@Override
protected TestTargetThread newTestTargetThread(
TestTargetThreadContainer container, int tid) {
return new TestTargetThread(container, tid) {
{
changeAttributes(List.of(),
AnnotatedTargetMethod.collectExports(MethodHandles.lookup(),
testModel, this),
"Methods");
}
@TargetMethod.Export("MyMethod")
public CompletableFuture<String> myMethod(
@TargetMethod.Param(
display = "I",
description = "An int param",
name = "i") int i,
@TargetMethod.Param(
display = "B",
description = "A boolean param",
name = "b") boolean b,
@TargetMethod.Param(
display = "S",
description = "A string param",
name = "s") String s,
@TargetMethod.Param(
display = "L",
description = "A long param",
name = "l") long l) {
return CompletableFuture
.completedFuture(i + "," + b + "," + s + "," + l);
}
};
}
};
}
};
mb.createTestModel();
mb.createTestProcessesAndThreads();
TargetMethod method = (TargetMethod) mb.testThread1.getCachedAttribute("MyMethod");
assertEquals(String.class, method.getReturnType());
assertEquals(TargetParameterMap.ofEntries(
Map.entry("i",
ParameterDescription.create(Integer.class, "i", true, null, "I",
"An int param")),
Map.entry("b",
ParameterDescription.create(Boolean.class, "b", true, null, "B",
"A boolean param")),
Map.entry("s",
ParameterDescription.create(String.class, "s", true, null, "S",
"A string param")),
Map.entry("l",
ParameterDescription.create(Long.class, "l", true, null, "L",
"A long param"))),
method.getParameters());
assertEquals("3,true,Hello,7", waitOn(method.invoke(Map.ofEntries(
Map.entry("b", true), Map.entry("i", 3), Map.entry("s", "Hello"),
Map.entry("l", 7L)))));
}
}

View file

@ -16,17 +16,18 @@
package ghidra.dbg.target.schema;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import org.junit.Test;
import ghidra.async.AsyncUtils;
import ghidra.dbg.agent.*;
import ghidra.dbg.target.*;
import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema;
import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
import ghidra.dbg.target.schema.TargetObjectSchema.*;
public class AnnotatedTargetObjectSchemaTest {
@ -384,4 +385,31 @@ public class AnnotatedTargetObjectSchemaTest {
AnnotatedSchemaContext ctx = new AnnotatedSchemaContext();
ctx.getSchemaForClass(TestAnnotatedTargetRootWithListedAttrsBadType.class);
}
@TargetObjectSchemaInfo
static class TestAnnotatedTargetRootWithExportedTargetMethod extends DefaultTargetModelRoot {
public TestAnnotatedTargetRootWithExportedTargetMethod(AbstractDebuggerObjectModel model,
String typeHint) {
super(model, typeHint);
}
@TargetMethod.Export("MyMethod")
public CompletableFuture<Void> myMethod() {
return AsyncUtils.NIL;
}
}
@Test
public void testAnnotatedRootSchemaWithExportedTargetMethod() {
AnnotatedSchemaContext ctx = new AnnotatedSchemaContext();
TargetObjectSchema schema =
ctx.getSchemaForClass(TestAnnotatedTargetRootWithExportedTargetMethod.class);
AttributeSchema methodSchema = schema.getAttributeSchema("MyMethod");
assertEquals(
new DefaultAttributeSchema("MyMethod", new SchemaName("Method"), true, true, true),
methodSchema);
assertTrue(
ctx.getSchema(new SchemaName("Method")).getInterfaces().contains(TargetMethod.class));
}
}

View file

@ -411,4 +411,12 @@
<attribute name="_order" schema="INT" hidden="yes" />
<attribute schema="VOID" />
</schema>
</context>
<schema name="Method" elementResync="NEVER" attributeResync="NEVER">
<interface name="Method" />
<element schema="VOID" />
<attribute name="_display" schema="STRING" fixed="yes" hidden="yes" />
<attribute name="_return_type" schema="TYPE" fixed="yes" hidden="yes" />
<attribute name="_parameters" schema="MAP_PARAMETERS" fixed="yes" hidden="yes" />
<attribute schema="VOID" />
</schema>
</context>